From 93d1b19a7ce1fba3a532cfb6e2b4059dfc636f95 Mon Sep 17 00:00:00 2001 From: Martin Villalba <14tinchov@gmail.com> Date: Sat, 11 Apr 2020 11:28:49 -0300 Subject: [PATCH] - Initial commit --- .gitignore | 27 +++ .rspec | 1 + .ruby-version | 1 + Gemfile | 31 +++ Gemfile.lock | 199 ++++++++++++++++++ README.md | 115 +--------- Rakefile | 8 + app/channels/application_cable/channel.rb | 6 + app/channels/application_cable/connection.rb | 6 + app/controllers/application_controller.rb | 4 + app/controllers/concerns/.keep | 0 app/controllers/stats_controller.rb | 8 + app/controllers/urls_controller.rb | 35 +++ app/jobs/application_job.rb | 4 + app/mailers/application_mailer.rb | 6 + app/models/application_record.rb | 5 + app/models/concerns/.keep | 0 app/models/url.rb | 22 ++ app/serializers/url_serializer.rb | 7 + app/views/layouts/mailer.html.erb | 13 ++ app/views/layouts/mailer.text.erb | 1 + bin/bundle | 117 ++++++++++ bin/rails | 11 + bin/rake | 11 + bin/setup | 35 +++ bin/spring | 18 ++ config.ru | 7 + config/application.rb | 26 +++ config/boot.rb | 6 + config/cable.yml | 10 + config/credentials.yml.enc | 1 + config/database.yml | 25 +++ config/environment.rb | 7 + config/environments/development.rb | 24 +++ config/environments/production.rb | 21 ++ config/environments/test.rb | 19 ++ .../application_controller_renderer.rb | 1 + config/initializers/backtrace_silencers.rb | 1 + config/initializers/cors.rb | 1 + .../initializers/filter_parameter_logging.rb | 3 + config/initializers/inflections.rb | 1 + config/initializers/mime_types.rb | 1 + config/initializers/wrap_parameters.rb | 5 + config/locales/en.yml | 33 +++ config/puma.rb | 9 + config/routes.rb | 7 + config/spring.rb | 8 + config/storage.yml | 34 +++ db/migrate/20200410235351_create_urls.rb | 14 ++ db/schema.rb | 12 ++ db/seeds.rb | 0 lib/tasks/.keep | 0 log/.keep | 0 public/robots.txt | 1 + spec/factories/urls.rb | 17 ++ spec/models/url_spec.rb | 18 ++ spec/rails_helper.rb | 41 ++++ spec/requests/stats_request_spec.rb | 17 ++ spec/requests/urls_request_spec.rb | 59 ++++++ spec/serializers/url_serializer_spec.rb | 36 ++++ spec/spec_helper.rb | 13 ++ spec/support/factory_bot.rb | 5 + storage/.keep | 0 tmp/.keep | 0 vendor/.keep | 0 65 files changed, 1068 insertions(+), 106 deletions(-) create mode 100644 .gitignore create mode 100644 .rspec create mode 100644 .ruby-version create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 Rakefile create mode 100644 app/channels/application_cable/channel.rb create mode 100644 app/channels/application_cable/connection.rb create mode 100644 app/controllers/application_controller.rb create mode 100644 app/controllers/concerns/.keep create mode 100644 app/controllers/stats_controller.rb create mode 100644 app/controllers/urls_controller.rb create mode 100644 app/jobs/application_job.rb create mode 100644 app/mailers/application_mailer.rb create mode 100644 app/models/application_record.rb create mode 100644 app/models/concerns/.keep create mode 100644 app/models/url.rb create mode 100644 app/serializers/url_serializer.rb create mode 100644 app/views/layouts/mailer.html.erb create mode 100644 app/views/layouts/mailer.text.erb create mode 100755 bin/bundle create mode 100755 bin/rails create mode 100755 bin/rake create mode 100755 bin/setup create mode 100755 bin/spring create mode 100644 config.ru create mode 100644 config/application.rb create mode 100644 config/boot.rb create mode 100644 config/cable.yml create mode 100644 config/credentials.yml.enc create mode 100644 config/database.yml create mode 100644 config/environment.rb create mode 100644 config/environments/development.rb create mode 100644 config/environments/production.rb create mode 100644 config/environments/test.rb create mode 100644 config/initializers/application_controller_renderer.rb create mode 100644 config/initializers/backtrace_silencers.rb create mode 100644 config/initializers/cors.rb create mode 100644 config/initializers/filter_parameter_logging.rb create mode 100644 config/initializers/inflections.rb create mode 100644 config/initializers/mime_types.rb create mode 100644 config/initializers/wrap_parameters.rb create mode 100644 config/locales/en.yml create mode 100644 config/puma.rb create mode 100644 config/routes.rb create mode 100644 config/spring.rb create mode 100644 config/storage.yml create mode 100644 db/migrate/20200410235351_create_urls.rb create mode 100644 db/schema.rb create mode 100644 db/seeds.rb create mode 100644 lib/tasks/.keep create mode 100644 log/.keep create mode 100644 public/robots.txt create mode 100644 spec/factories/urls.rb create mode 100644 spec/models/url_spec.rb create mode 100644 spec/rails_helper.rb create mode 100644 spec/requests/stats_request_spec.rb create mode 100644 spec/requests/urls_request_spec.rb create mode 100644 spec/serializers/url_serializer_spec.rb create mode 100644 spec/spec_helper.rb create mode 100644 spec/support/factory_bot.rb create mode 100644 storage/.keep create mode 100644 tmp/.keep create mode 100644 vendor/.keep diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4d6cefb --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead: +# git config --global core.excludesfile '~/.gitignore_global' + +# Ignore bundler config. +/.bundle + +# Ignore the default SQLite database. +/db/*.sqlite3 +/db/*.sqlite3-journal +/db/*.sqlite3-* + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore uploaded files in development. +/storage/* +!/storage/.keep +.byebug_history + +# Ignore master key for decrypting credentials and more. +/config/master.key diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..c99d2e7 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..4560fb9 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +ruby-2.6.3 diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..bfcccba --- /dev/null +++ b/Gemfile @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' +git_source(:github) { |repo| "https://github.com/#{repo}.git" } + +ruby '2.6.3' + +gem 'bootsnap', '>= 1.4.2', require: false +gem 'fast_jsonapi' +gem 'puma', '~> 4.1' +gem 'rails', '~> 6.0.2', '>= 6.0.2.2' +gem 'sqlite3', '~> 1.4' + +group :development, :test do + gem 'byebug', platforms: %i[mri mingw x64_mingw] + gem 'factory_bot_rails' + gem 'rspec-rails' + gem 'shoulda-matchers' +end + +group :test do + gem 'database_cleaner' +end + +group :development do + gem 'listen', '>= 3.0.5', '< 3.2' + gem 'spring' + gem 'spring-watcher-listen', '~> 2.0.0' +end + +gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..ea093eb --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,199 @@ +GEM + remote: https://rubygems.org/ + specs: + actioncable (6.0.2.2) + actionpack (= 6.0.2.2) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + actionmailbox (6.0.2.2) + actionpack (= 6.0.2.2) + activejob (= 6.0.2.2) + activerecord (= 6.0.2.2) + activestorage (= 6.0.2.2) + activesupport (= 6.0.2.2) + mail (>= 2.7.1) + actionmailer (6.0.2.2) + actionpack (= 6.0.2.2) + actionview (= 6.0.2.2) + activejob (= 6.0.2.2) + mail (~> 2.5, >= 2.5.4) + rails-dom-testing (~> 2.0) + actionpack (6.0.2.2) + actionview (= 6.0.2.2) + activesupport (= 6.0.2.2) + rack (~> 2.0, >= 2.0.8) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actiontext (6.0.2.2) + actionpack (= 6.0.2.2) + activerecord (= 6.0.2.2) + activestorage (= 6.0.2.2) + activesupport (= 6.0.2.2) + nokogiri (>= 1.8.5) + actionview (6.0.2.2) + activesupport (= 6.0.2.2) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.1, >= 1.2.0) + activejob (6.0.2.2) + activesupport (= 6.0.2.2) + globalid (>= 0.3.6) + activemodel (6.0.2.2) + activesupport (= 6.0.2.2) + activerecord (6.0.2.2) + activemodel (= 6.0.2.2) + activesupport (= 6.0.2.2) + activestorage (6.0.2.2) + actionpack (= 6.0.2.2) + activejob (= 6.0.2.2) + activerecord (= 6.0.2.2) + marcel (~> 0.3.1) + activesupport (6.0.2.2) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 0.7, < 2) + minitest (~> 5.1) + tzinfo (~> 1.1) + zeitwerk (~> 2.2) + bootsnap (1.4.6) + msgpack (~> 1.0) + builder (3.2.4) + byebug (11.1.1) + concurrent-ruby (1.1.6) + crass (1.0.6) + database_cleaner (1.8.4) + diff-lcs (1.3) + erubi (1.9.0) + factory_bot (5.0.2) + activesupport (>= 4.2.0) + factory_bot_rails (5.0.2) + factory_bot (~> 5.0.2) + railties (>= 4.2.0) + fast_jsonapi (1.5) + activesupport (>= 4.2) + ffi (1.12.2) + globalid (0.4.2) + activesupport (>= 4.2.0) + i18n (1.8.2) + concurrent-ruby (~> 1.0) + listen (3.1.5) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + ruby_dep (~> 1.2) + loofah (2.5.0) + crass (~> 1.0.2) + nokogiri (>= 1.5.9) + mail (2.7.1) + mini_mime (>= 0.1.1) + marcel (0.3.3) + mimemagic (~> 0.3.2) + method_source (1.0.0) + mimemagic (0.3.4) + mini_mime (1.0.2) + mini_portile2 (2.4.0) + minitest (5.14.0) + msgpack (1.3.3) + nio4r (2.5.2) + nokogiri (1.10.9) + mini_portile2 (~> 2.4.0) + puma (4.3.3) + nio4r (~> 2.0) + rack (2.2.2) + rack-test (1.1.0) + rack (>= 1.0, < 3) + rails (6.0.2.2) + actioncable (= 6.0.2.2) + actionmailbox (= 6.0.2.2) + actionmailer (= 6.0.2.2) + actionpack (= 6.0.2.2) + actiontext (= 6.0.2.2) + actionview (= 6.0.2.2) + activejob (= 6.0.2.2) + activemodel (= 6.0.2.2) + activerecord (= 6.0.2.2) + activestorage (= 6.0.2.2) + activesupport (= 6.0.2.2) + bundler (>= 1.3.0) + railties (= 6.0.2.2) + sprockets-rails (>= 2.0.0) + rails-dom-testing (2.0.3) + activesupport (>= 4.2.0) + nokogiri (>= 1.6) + rails-html-sanitizer (1.3.0) + loofah (~> 2.3) + railties (6.0.2.2) + actionpack (= 6.0.2.2) + activesupport (= 6.0.2.2) + method_source + rake (>= 0.8.7) + thor (>= 0.20.3, < 2.0) + rake (13.0.1) + rb-fsevent (0.10.3) + rb-inotify (0.10.1) + ffi (~> 1.0) + rspec-core (3.9.1) + rspec-support (~> 3.9.1) + rspec-expectations (3.9.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.9.0) + rspec-mocks (3.9.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.9.0) + rspec-rails (4.0.0) + actionpack (>= 4.2) + activesupport (>= 4.2) + railties (>= 4.2) + rspec-core (~> 3.9) + rspec-expectations (~> 3.9) + rspec-mocks (~> 3.9) + rspec-support (~> 3.9) + rspec-support (3.9.2) + ruby_dep (1.5.0) + shoulda-matchers (4.3.0) + activesupport (>= 4.2.0) + spring (2.1.0) + spring-watcher-listen (2.0.1) + listen (>= 2.7, < 4.0) + spring (>= 1.2, < 3.0) + sprockets (4.0.0) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + sprockets-rails (3.2.1) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) + sqlite3 (1.4.2) + thor (1.0.1) + thread_safe (0.3.6) + tzinfo (1.2.7) + thread_safe (~> 0.1) + websocket-driver (0.7.1) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.4) + zeitwerk (2.3.0) + +PLATFORMS + ruby + +DEPENDENCIES + bootsnap (>= 1.4.2) + byebug + database_cleaner + factory_bot_rails + fast_jsonapi + listen (>= 3.0.5, < 3.2) + puma (~> 4.1) + rails (~> 6.0.2, >= 6.0.2.2) + rspec-rails + shoulda-matchers + spring + spring-watcher-listen (~> 2.0.0) + sqlite3 (~> 1.4) + tzinfo-data + +RUBY VERSION + ruby 2.6.3p62 + +BUNDLED WITH + 1.17.2 diff --git a/README.md b/README.md index 73d26f9..7c1519c 100644 --- a/README.md +++ b/README.md @@ -1,112 +1,15 @@ -# Code Challenge +# README -Create a micro service to shorten urls like bit.ly or TinyURL do. +* Ruby version -## Rules +ruby-2.6.3 -1. The service must expose HTTP endpoints according to the API Docs below. -2. Use your technology of choice, there's no restrictions. Instructions for the installation must be detailed in the INSTALL.md file. -3. Write the tests you consider necessary. +* Configuration -------------------------------------------------------------------------- +bundle install +rails db:migrate +rails s -## API Docs +* How to run the test suite -### POST /urls - -``` -POST /urls -Content-Type: "application/json" - -{ - "url": "http://example.com", - "code": "example" -} -``` - -##### Params: - -* **url**: URL to shorten. Required -* code: Desired shortcode. Alphanumeric, case-sensitive 6 chars lenght. - -Note: If code is not provided, a random code, with the same constraints, must be generated - -##### Response: - -``` -201 Created -Content-Type: "application/json" - -{ - "code": :shortcode -} -``` - -##### Errors: - -* Bad Request: If ```url``` is not present -* Conflict: If the the desired shortcode is already in use. -* Unprocessable Entity: If the shortcode doesn't doesn't comply with its description. - - -### GET /:code - -``` -GET /:code -Content-Type: "application/json" -``` - -##### Params: -* **code**: Encoded URL shortcode - -##### Response - -It's a redirect response including the target URL in its `Location` header. - -``` -HTTP/1.1 302 Found -Location: http://www.example.com -``` - -##### Errors - -* Not Found: If the `shortcode` cannot be found - -### GET /:code/stats - -``` -GET /:code/stats -Content-Type: "application/json" -``` - -##### Params: -* **code**: Encoded URL shortcode - -##### Response - -``` -200 OK -Content-Type: "application/json" - -{ - "created_at": "2012-04-23T18:25:43.511Z", - "last_usage": "2012-04-23T18:25:43.511Z", - "usage_count": 1 -} -``` - -* **`start_date`**: [ISO8601](http://en.wikipedia.org/wiki/ISO_8601) formatted date when the shortened URL was created -* **`usage_count`**: Number of requests to the endpoint `GET /code` -* `last_usage`: Date of the last time the shortened URL was requested. Not included if it has never been requested. - -##### Errors - -* Not Found: If the `shortcode` cannot be found - -## Delivery Steps: - -1. Fork this repo to your own Github account. -2. Implement the functionality, including any instructions to setup and run the application. -3. Submit a PR to the `master` branch of this repository. - -Thank you and good luck! +rspec spec \ No newline at end of file diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..488c551 --- /dev/null +++ b/Rakefile @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative 'config/application' + +Rails.application.load_tasks diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb new file mode 100644 index 0000000..9aec230 --- /dev/null +++ b/app/channels/application_cable/channel.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module ApplicationCable + class Channel < ActionCable::Channel::Base + end +end diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb new file mode 100644 index 0000000..8d6c2a1 --- /dev/null +++ b/app/channels/application_cable/connection.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module ApplicationCable + class Connection < ActionCable::Connection::Base + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 0000000..13c271f --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class ApplicationController < ActionController::API +end diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/controllers/stats_controller.rb b/app/controllers/stats_controller.rb new file mode 100644 index 0000000..d9f214c --- /dev/null +++ b/app/controllers/stats_controller.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class StatsController < ApplicationController + def show + @url = Url.find_by_code(params[:code]) + render json: UrlSerializer.new(@url).serializable_hash[:data][:attributes] + end +end diff --git a/app/controllers/urls_controller.rb b/app/controllers/urls_controller.rb new file mode 100644 index 0000000..bc3ed0b --- /dev/null +++ b/app/controllers/urls_controller.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +class UrlsController < ApplicationController + after_action :set_location + + def show + @url = Url.find_by_code(params[:code]) + if @url + @url.visited + render status: 302 + else + render status: 400 + end + end + + def create + @url = Url.create(url_params) + + if @url.valid? + render json: { code: @url.code }, status: 201 + else + render status: 400 + end + end + + private + + def url_params + params.permit(:url, :code) + end + + def set_location + response.headers['Location'] = @url.url if @url + end +end diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 0000000..d92ffdd --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class ApplicationJob < ActiveJob::Base +end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb new file mode 100644 index 0000000..d84cb6e --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class ApplicationMailer < ActionMailer::Base + default from: 'from@example.com' + layout 'mailer' +end diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 0000000..71fbba5 --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/models/url.rb b/app/models/url.rb new file mode 100644 index 0000000..57b3fac --- /dev/null +++ b/app/models/url.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class Url < ApplicationRecord + before_validation :set_default_code + + validates :code, length: { maximum: 6 } + validates :code, uniqueness: true + validates :url, presence: true + + def visited + update( + last_usage: DateTime.now.in_time_zone('UTC'), + usage_count: usage_count.to_i + 1 + ) + end + + private + + def set_default_code + self.code = SecureRandom.alphanumeric(6) unless code + end +end diff --git a/app/serializers/url_serializer.rb b/app/serializers/url_serializer.rb new file mode 100644 index 0000000..bebe4cd --- /dev/null +++ b/app/serializers/url_serializer.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class UrlSerializer + include FastJsonapi::ObjectSerializer + attributes :created_at, :last_usage, :usage_count + attribute :last_usage, if: proc { |record| !record.last_usage.nil? } +end diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb new file mode 100644 index 0000000..cbd34d2 --- /dev/null +++ b/app/views/layouts/mailer.html.erb @@ -0,0 +1,13 @@ + + +
+ + + + + + <%= yield %> + + diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb new file mode 100644 index 0000000..37f0bdd --- /dev/null +++ b/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/bin/bundle b/bin/bundle new file mode 100755 index 0000000..2155a52 --- /dev/null +++ b/bin/bundle @@ -0,0 +1,117 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'bundle' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require 'rubygems' + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($PROGRAM_NAME) == File.expand_path(__FILE__) + end + + def env_var_version + ENV['BUNDLER_VERSION'] + end + + def cli_arg_version + return unless invoked_as_script? # don't want to hijack other binstubs + unless 'update'.start_with?(ARGV.first || ' ') + return + end # must be running `bundle update` + + bundler_version = nil + update_index = nil + ARGV.each_with_index do |a, i| + if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN + bundler_version = a + end + unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ + next + end + + bundler_version = Regexp.last_match(1) || '>= 0.a' + update_index = i + end + bundler_version + end + + def gemfile + gemfile = ENV['BUNDLE_GEMFILE'] + return gemfile if gemfile && !gemfile.empty? + + File.expand_path('../Gemfile', __dir__) + end + + def lockfile + lockfile = + case File.basename(gemfile) + when 'gems.rb' then gemfile.sub(/\.rb$/, gemfile) + else "#{gemfile}.lock" + end + File.expand_path(lockfile) + end + + def lockfile_version + return unless File.file?(lockfile) + + lockfile_contents = File.read(lockfile) + unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ + return + end + + Regexp.last_match(1) + end + + def bundler_version + @bundler_version ||= begin + env_var_version || cli_arg_version || + lockfile_version || "#{Gem::Requirement.default}.a" + end + end + + def load_bundler! + ENV['BUNDLE_GEMFILE'] ||= gemfile + + # must dup string for RG < 1.8 compatibility + activate_bundler(bundler_version.dup) + end + + def activate_bundler(bundler_version) + if Gem::Version.correct?(bundler_version) && Gem::Version.new(bundler_version).release < Gem::Version.new('2.0') + bundler_version = '< 2' + end + gem_error = activation_error_handling do + gem 'bundler', bundler_version + end + return if gem_error.nil? + + require_error = activation_error_handling do + require 'bundler/version' + end + if require_error.nil? && Gem::Requirement.new(bundler_version).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + return + end + + warn "Activating bundler (#{bundler_version}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_version}'`" + exit 42 + end + + def activation_error_handling + yield + nil + rescue StandardError, LoadError => e + e + end +end + +m.load_bundler! + +load Gem.bin_path('bundler', 'bundle') if m.invoked_as_script? diff --git a/bin/rails b/bin/rails new file mode 100755 index 0000000..3504c3f --- /dev/null +++ b/bin/rails @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +begin + load File.expand_path('spring', __dir__) +rescue LoadError => e + raise unless e.message.include?('spring') +end +APP_PATH = File.expand_path('../config/application', __dir__) +require_relative '../config/boot' +require 'rails/commands' diff --git a/bin/rake b/bin/rake new file mode 100755 index 0000000..1fe6cf0 --- /dev/null +++ b/bin/rake @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +begin + load File.expand_path('spring', __dir__) +rescue LoadError => e + raise unless e.message.include?('spring') +end +require_relative '../config/boot' +require 'rake' +Rake.application.run diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..65a6ca7 --- /dev/null +++ b/bin/setup @@ -0,0 +1,35 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'fileutils' + +# path to your application root. +APP_ROOT = File.expand_path('..', __dir__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +FileUtils.chdir APP_ROOT do + # This script is a way to setup or update your development environment automatically. + # This script is idempotent, so that you can run it at anytime and get an expectable outcome. + # Add necessary setup steps to this file. + + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') + + # puts "\n== Copying sample files ==" + # unless File.exist?('config/database.yml') + # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' + # end + + puts "\n== Preparing database ==" + system! 'bin/rails db:prepare' + + puts "\n== Removing old logs and tempfiles ==" + system! 'bin/rails log:clear tmp:clear' + + puts "\n== Restarting application server ==" + system! 'bin/rails restart' +end diff --git a/bin/spring b/bin/spring new file mode 100755 index 0000000..1c6eabf --- /dev/null +++ b/bin/spring @@ -0,0 +1,18 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# This file loads Spring without using Bundler, in order to be fast. +# It gets overwritten when you run the `spring binstub` command. + +unless defined?(Spring) + require 'rubygems' + require 'bundler' + + lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) + spring = lockfile.specs.detect { |spec| spec.name == 'spring' } + if spring + Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path + gem 'spring', spring.version + require 'spring/binstub' + end +end diff --git a/config.ru b/config.ru new file mode 100644 index 0000000..842bccc --- /dev/null +++ b/config.ru @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# This file is used by Rack-based servers to start the application. + +require_relative 'config/environment' + +run Rails.application diff --git a/config/application.rb b/config/application.rb new file mode 100644 index 0000000..88bab0e --- /dev/null +++ b/config/application.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require_relative 'boot' + +require 'rails' +# Pick the frameworks you want: +require 'active_model/railtie' +require 'active_job/railtie' +require 'active_record/railtie' +require 'active_storage/engine' +require 'action_controller/railtie' +require 'action_mailer/railtie' +require 'action_mailbox/engine' +require 'action_text/engine' +require 'action_view/railtie' +require 'action_cable/engine' +# require "sprockets/railtie" +require 'rails/test_unit/railtie' +Bundler.require(*Rails.groups) + +module CodeTest + class Application < Rails::Application + config.load_defaults 6.0 + config.api_only = true + end +end diff --git a/config/boot.rb b/config/boot.rb new file mode 100644 index 0000000..c04863f --- /dev/null +++ b/config/boot.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) + +require 'bundler/setup' # Set up gems listed in the Gemfile. +require 'bootsnap/setup' # Speed up boot time by caching expensive operations. diff --git a/config/cable.yml b/config/cable.yml new file mode 100644 index 0000000..4242ab1 --- /dev/null +++ b/config/cable.yml @@ -0,0 +1,10 @@ +development: + adapter: async + +test: + adapter: test + +production: + adapter: redis + url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> + channel_prefix: code_test_production diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc new file mode 100644 index 0000000..8310c2c --- /dev/null +++ b/config/credentials.yml.enc @@ -0,0 +1 @@ +dvmcfTbHdN8RrmeyzNfp0pRBHVHU6okLAtMCa5913dKchWRb0qD4PljARF0/fE0IwiigajGUwvHU+ATpRyrLRqYL2BFLbvLTmC2ttl03BHp1F1yO+iNw1PIiFqT6TlfWZ8sASGxwZWjBmGJp2m4PecklHB4NJAfSKqjkB+/DCTppSs6iB+c3kzkhXvXFtT4q4dzIV/W7ECU8iEXnthmppmXTu7g1lbRk52WpXg4dMEKCcagBuecKHBEVRsrO3zZwFQsGPdEi9C99Q9BGQnOJifXFJOYuTxfAoVw0SlNCAkZsm2x9mfL+l2wgPFEIwLebUuNYcpUDBHHPyO2svqCcODzih26ycM++xqnWUXSqukRBfeFazLNnhLwdFzywbnEWFHBgEuUOzEPF2OQx18+83oV3BPL4V8iLl+xA--OS1gS5q9DO8LzOFP--YNh1r7r2mB0i5AeVUhwCAA== \ No newline at end of file diff --git a/config/database.yml b/config/database.yml new file mode 100644 index 0000000..4a8a1b2 --- /dev/null +++ b/config/database.yml @@ -0,0 +1,25 @@ +# SQLite. Versions 3.8.0 and up are supported. +# gem install sqlite3 +# +# Ensure the SQLite 3 gem is defined in your Gemfile +# gem 'sqlite3' +# +default: &default + adapter: sqlite3 + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + timeout: 5000 + +development: + <<: *default + database: db/development.sqlite3 + +# 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: db/test.sqlite3 + +production: + <<: *default + database: db/production.sqlite3 diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 0000000..d5abe55 --- /dev/null +++ b/config/environment.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# Load the Rails application. +require_relative 'application' + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb new file mode 100644 index 0000000..64b3702 --- /dev/null +++ b/config/environments/development.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +Rails.application.configure do + config.cache_classes = false + config.eager_load = false + config.consider_all_requests_local = true + if Rails.root.join('tmp', 'caching-dev.txt').exist? + config.cache_store = :memory_store + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{2.days.to_i}" + } + else + config.action_controller.perform_caching = false + + config.cache_store = :null_store + end + config.active_storage.service = :local + config.action_mailer.raise_delivery_errors = false + config.action_mailer.perform_caching = false + config.active_support.deprecation = :log + config.active_record.migration_error = :page_load + config.active_record.verbose_query_logs = true + config.file_watcher = ActiveSupport::EventedFileUpdateChecker +end diff --git a/config/environments/production.rb b/config/environments/production.rb new file mode 100644 index 0000000..4de9214 --- /dev/null +++ b/config/environments/production.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +Rails.application.configure do + config.cache_classes = true + config.eager_load = true + config.consider_all_requests_local = false + config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? + config.active_storage.service = :local + config.log_level = :debug + config.log_tags = [:request_id] + config.action_mailer.perform_caching = false + config.i18n.fallbacks = true + config.active_support.deprecation = :notify + config.log_formatter = ::Logger::Formatter.new + if ENV['RAILS_LOG_TO_STDOUT'].present? + logger = ActiveSupport::Logger.new(STDOUT) + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + end + config.active_record.dump_schema_after_migration = false +end diff --git a/config/environments/test.rb b/config/environments/test.rb new file mode 100644 index 0000000..3a7b66a --- /dev/null +++ b/config/environments/test.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +Rails.application.configure do + config.cache_classes = false + config.eager_load = false + config.public_file_server.enabled = true + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{1.hour.to_i}" + } + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + config.cache_store = :null_store + config.action_dispatch.show_exceptions = false + config.action_controller.allow_forgery_protection = false + config.active_storage.service = :test + config.action_mailer.perform_caching = false + config.action_mailer.delivery_method = :test + config.active_support.deprecation = :stderr +end diff --git a/config/initializers/application_controller_renderer.rb b/config/initializers/application_controller_renderer.rb new file mode 100644 index 0000000..8e9b8f9 --- /dev/null +++ b/config/initializers/application_controller_renderer.rb @@ -0,0 +1 @@ +# frozen_string_literal: true diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb new file mode 100644 index 0000000..8e9b8f9 --- /dev/null +++ b/config/initializers/backtrace_silencers.rb @@ -0,0 +1 @@ +# frozen_string_literal: true diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb new file mode 100644 index 0000000..8e9b8f9 --- /dev/null +++ b/config/initializers/cors.rb @@ -0,0 +1 @@ +# frozen_string_literal: true diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000..e25c8cc --- /dev/null +++ b/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +Rails.application.config.filter_parameters += [:password] diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb new file mode 100644 index 0000000..8e9b8f9 --- /dev/null +++ b/config/initializers/inflections.rb @@ -0,0 +1 @@ +# frozen_string_literal: true diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb new file mode 100644 index 0000000..8e9b8f9 --- /dev/null +++ b/config/initializers/mime_types.rb @@ -0,0 +1 @@ +# frozen_string_literal: true diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb new file mode 100644 index 0000000..5733a40 --- /dev/null +++ b/config/initializers/wrap_parameters.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +ActiveSupport.on_load(:action_controller) do + wrap_parameters format: [:json] +end diff --git a/config/locales/en.yml b/config/locales/en.yml new file mode 100644 index 0000000..cf9b342 --- /dev/null +++ b/config/locales/en.yml @@ -0,0 +1,33 @@ +# Files in the config/locales directory are used for internationalization +# and are automatically loaded by Rails. If you want to use locales other +# than English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t 'hello' +# +# In views, this is aliased to just `t`: +# +# <%= t('hello') %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# The following keys must be escaped otherwise they will not be retrieved by +# the default I18n backend: +# +# true, false, on, off, yes, no +# +# Instead, surround them with single quotes. +# +# en: +# 'true': 'foo' +# +# To learn more, please read the Rails Internationalization guide +# available at https://guides.rubyonrails.org/i18n.html. + +en: + hello: "Hello world" diff --git a/config/puma.rb b/config/puma.rb new file mode 100644 index 0000000..bd8c907 --- /dev/null +++ b/config/puma.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +max_threads_count = ENV.fetch('RAILS_MAX_THREADS') { 5 } +min_threads_count = ENV.fetch('RAILS_MIN_THREADS') { max_threads_count } +threads min_threads_count, max_threads_count +port ENV.fetch('PORT') { 3000 } +environment ENV.fetch('RAILS_ENV') { 'development' } +pidfile ENV.fetch('PIDFILE') { 'tmp/pids/server.pid' } +plugin :tmp_restart diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 0000000..cb8f5de --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +Rails.application.routes.draw do + post 'urls', to: 'urls#create' + get '/:code', to: 'urls#show', as: 'url' + get '/:code/stats', to: 'stats#show', as: 'url_stats' +end diff --git a/config/spring.rb b/config/spring.rb new file mode 100644 index 0000000..93cd0ff --- /dev/null +++ b/config/spring.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +Spring.watch( + '.ruby-version', + '.rbenv-vars', + 'tmp/restart.txt', + 'tmp/caching-dev.txt' +) diff --git a/config/storage.yml b/config/storage.yml new file mode 100644 index 0000000..d32f76e --- /dev/null +++ b/config/storage.yml @@ -0,0 +1,34 @@ +test: + service: Disk + root: <%= Rails.root.join("tmp/storage") %> + +local: + service: Disk + root: <%= Rails.root.join("storage") %> + +# Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) +# amazon: +# service: S3 +# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> +# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> +# region: us-east-1 +# bucket: your_own_bucket + +# Remember not to checkin your GCS keyfile to a repository +# google: +# service: GCS +# project: your_project +# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> +# bucket: your_own_bucket + +# Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) +# microsoft: +# service: AzureStorage +# storage_account_name: your_account_name +# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> +# container: your_container_name + +# mirror: +# service: Mirror +# primary: local +# mirrors: [ amazon, google, microsoft ] diff --git a/db/migrate/20200410235351_create_urls.rb b/db/migrate/20200410235351_create_urls.rb new file mode 100644 index 0000000..7ab6d52 --- /dev/null +++ b/db/migrate/20200410235351_create_urls.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class CreateUrls < ActiveRecord::Migration[6.0] + def change + create_table :urls do |t| + t.string :url + t.string :code + t.datetime :last_usage + t.integer :usage_count + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 0000000..7e91361 --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +ActiveRecord::Schema.define(version: 20_200_410_235_351) do + create_table 'urls', force: :cascade do |t| + t.string 'url' + t.string 'code' + t.datetime 'last_usage' + t.integer 'usage_count' + t.datetime 'created_at', precision: 6, null: false + t.datetime 'updated_at', precision: 6, null: false + end +end diff --git a/db/seeds.rb b/db/seeds.rb new file mode 100644 index 0000000..e69de29 diff --git a/lib/tasks/.keep b/lib/tasks/.keep new file mode 100644 index 0000000..e69de29 diff --git a/log/.keep b/log/.keep new file mode 100644 index 0000000..e69de29 diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..c19f78a --- /dev/null +++ b/public/robots.txt @@ -0,0 +1 @@ +# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file diff --git a/spec/factories/urls.rb b/spec/factories/urls.rb new file mode 100644 index 0000000..f7b4809 --- /dev/null +++ b/spec/factories/urls.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :url do + url { 'http://example.com' } + code { 'exmple' } + + trait :without_code do + code { nil } + end + + trait :visited do + last_usage { DateTime.now } + usage_count { 1 } + end + end +end diff --git a/spec/models/url_spec.rb b/spec/models/url_spec.rb new file mode 100644 index 0000000..c766bb8 --- /dev/null +++ b/spec/models/url_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Url, type: :model do + describe 'validations' do + subject { build(:url) } + + it { should validate_presence_of(:url) } + it { should validate_uniqueness_of(:code) } + it { should validate_length_of(:code).is_at_most(6) } + end + + it '#set_default_code' do + url = create(:url, :without_code) + expect(url.code).not_to be_nil + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb new file mode 100644 index 0000000..a140aa6 --- /dev/null +++ b/spec/rails_helper.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' +ENV['RAILS_ENV'] ||= 'test' +require File.expand_path('../config/environment', __dir__) +if Rails.env.production? + abort('The Rails environment is running in production mode!') +end +require 'rspec/rails' +require 'support/factory_bot' + +begin + ActiveRecord::Migration.maintain_test_schema! +rescue ActiveRecord::PendingMigrationError => e + puts e.to_s.strip + exit 1 +end + +RSpec.configure do |config| + config.fixture_path = "#{::Rails.root}/spec/fixtures" + config.use_transactional_fixtures = true + config.infer_spec_type_from_file_location! + config.filter_rails_from_backtrace! + config.include(Shoulda::Matchers::ActiveModel, type: :model) + config.include(Shoulda::Matchers::ActiveRecord, type: :model) + config.before(:suite) do + DatabaseCleaner.clean_with :truncation + end + + config.before(:each) do + DatabaseCleaner.strategy = :transaction + end + + config.before(:each) do + DatabaseCleaner.start + end + + config.after(:each) do + DatabaseCleaner.clean + end +end diff --git a/spec/requests/stats_request_spec.rb b/spec/requests/stats_request_spec.rb new file mode 100644 index 0000000..f73f037 --- /dev/null +++ b/spec/requests/stats_request_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Stats', type: :request do + describe 'GET show' do + let(:url) { create(:url) } + before { get url_stats_path(url.code) } + + it { expect(response).to have_http_status(:success) } + + it 'return UrlSerializer' do + serializer = UrlSerializer.new(url).serializable_hash[:data][:attributes] + expect(response.body).to eq serializer.to_json + end + end +end diff --git a/spec/requests/urls_request_spec.rb b/spec/requests/urls_request_spec.rb new file mode 100644 index 0000000..26c104c --- /dev/null +++ b/spec/requests/urls_request_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Urls', type: :request do + describe 'POST /create' do + let(:valid_params) { { url: 'http://example.com', code: 'exmple' } } + + it 'check create a new Url' do + expect { post urls_path, params: valid_params } + .to change { Url.count }.by(1) + end + + context 'with valid params' do + let(:last_url) { Url.last } + before { post urls_path, params: valid_params } + + it { expect(response).to have_http_status(201) } + it { expect(last_url.url).to eq(valid_params[:url]) } + it { expect(last_url.code).to eq(valid_params[:code]) } + it { expect(JSON.parse(response.body)['code']).to eq valid_params[:code] } + end + + context 'with valid params' do + let(:invalid_params) { { code: 'example' } } + before { post urls_path, params: invalid_params } + + it { expect(response).to have_http_status(400) } + end + end + + describe 'GET /:code' do + let!(:new_url) { create(:url) } + + it 'check usage_count increment updated' do + expect { get url_path(new_url.code) } + .to change { new_url.reload.usage_count }.from(nil).to(1) + end + + context 'with valid params' do + before { get url_path(new_url.code) } + + it { expect(response).to have_http_status(302) } + it 'Location header?' do + expect(response.headers['Location']).to eq new_url.url + end + + it 'check last_usage was updated' do + expect(new_url.reload.last_usage.round) + .to eq DateTime.now.in_time_zone('UTC').round + end + end + + context 'with invalid params' do + before { get url_path('not_code') } + it { expect(response).to have_http_status(400) } + end + end +end diff --git a/spec/serializers/url_serializer_spec.rb b/spec/serializers/url_serializer_spec.rb new file mode 100644 index 0000000..a32968f --- /dev/null +++ b/spec/serializers/url_serializer_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe UrlSerializer, type: :serializer do + describe 'Url is visited' do + let(:url) { create(:url, :visited) } + let(:url_serializer) { UrlSerializer.new(url) } + + it 'is last_usage present?' do + hash_expected = { + created_at: url.created_at, + last_usage: url.last_usage, + usage_count: url.usage_count + } + + expect(url_serializer.serializable_hash[:data][:attributes]) + .to eq hash_expected + end + end + + describe 'Url is not visited' do + let(:url) { create(:url) } + let(:url_serializer) { UrlSerializer.new(url) } + + it 'is not last_usage present?' do + hash_expected = { + created_at: url.created_at, + usage_count: url.usage_count + } + + expect(url_serializer.serializable_hash[:data][:attributes]) + .to eq hash_expected + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..a7d360f --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + config.expect_with :rspec do |expectations| + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + config.mock_with :rspec do |mocks| + mocks.verify_partial_doubles = true + end + + config.shared_context_metadata_behavior = :apply_to_host_groups +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 diff --git a/storage/.keep b/storage/.keep new file mode 100644 index 0000000..e69de29 diff --git a/tmp/.keep b/tmp/.keep new file mode 100644 index 0000000..e69de29 diff --git a/vendor/.keep b/vendor/.keep new file mode 100644 index 0000000..e69de29