diff --git a/.env.example b/.env.example index f2f5911..972d7cf 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,37 @@ -# Add your API tokens here +# Port to serve application +PORT=5000 + +# Host name with PORT of the application +HOST=localhost:5000 + +# CORS hostnames based on https://github.com/cyu/rack-cors +# CORS_ORIGINS= + +# Current environment RACK_ENV=development -ALLOW_REQUESTS_FROM=localhost:4000 -SECRET_KEY_BASE=development -# ROLLBAR_ACCESS_TOKEN= +# Base secret key +SECRET_KEY_BASE=development_secret + +# Database name prefix +# DATABASE_NAME=paste_database_prefix_here + +# Specify assets server host name, eg.: d2oek0c5zwe48d.cloudfront.net +ASSET_HOST=lvh.me:5000 +# S3_ASSET_HOST=https://d1ltghz9ehkb4n.cloudfront.net + +# Set Rollbar key for the app +# ROLLBAR_ACCESS_TOKEN=your_key_here + +# We send email using this "from" address +MAILER_SENDER_ADDRESS=noreply@example.com + +# Enable basic auth to close the app from unauthorized viewers +# AUTH_BASIC_REALM=your_application_name +# AUTH_BASIC_PASS=your_password_here + +# Set single hostname +# CANONICAL_HOST=paste_single_hostname_here + +# Comma separated list of IPs to access admin staff like RackMiniProfiler, Sidekiq Web, Flipper UI +IP_WHITELIST=127.0.0.1 diff --git a/.erdconfig b/.erdconfig new file mode 100644 index 0000000..e5fce58 --- /dev/null +++ b/.erdconfig @@ -0,0 +1,6 @@ +# more about rails-erd config file you can find +# here: https://github.com/voormedia/rails-erd#configuration +# and here: http://voormedia.github.io/rails-erd/customise.html + +filename: doc/entity-relationship +filetype: png diff --git a/.gitignore b/.gitignore index 2724481..aa312a9 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ vendor/ruby .sass-cache ._* .env +doc/entity-relationship.png diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..fb62bd7 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,104 @@ +require: rubocop-rspec + +Rails: + Enabled: true + +AllCops: + TargetRubyVersion: 2.3 + DisplayCopNames: true + Exclude: + - bin/**/* + - db/**/* + - vendor/**/* + - node_modules/**/* + +Rails/UnknownEnv: + Environments: + - production + - development + - test + - staging + +Rails/HasManyOrHasOneDependent: + Enabled: true + +Rails/HasAndBelongsToMany: + Enabled: false + +Rails/OutputSafety: + Enabled: false + +RSpec/MultipleExpectations: + Enabled: false + +RSpec/ExampleLength: + Enabled: false + +RSpec/RepeatedDescription: + Enabled: false + +Capybara/FeatureMethods: + Enabled: false + +RSpec/MessageSpies: + EnforcedStyle: receive + +RSpec/EmptyExampleGroup: + Exclude: + - spec/api/**/* + +Style/AndOr: + Enabled: false + +Style/Documentation: + Enabled: false + +Style/MethodCalledOnDoEndBlock: + Enabled: true + +Style/CollectionMethods: + Enabled: true + +Style/SymbolArray: + Enabled: true + +Style/StringLiterals: + EnforcedStyle: double_quotes + ConsistentQuotesInMultiline: true + +Style/EmptyMethod: + EnforcedStyle: expanded + SupportedStyles: + - compact + - expanded + +Style/FrozenStringLiteralComment: + Enabled: false + +Style/StringMethods: + Enabled: true + +Layout/MultilineMethodCallIndentation: + EnforcedStyle: indented + +Layout/AlignParameters: + EnforcedStyle: with_fixed_indentation + SupportedStyles: + - with_first_parameter + - with_fixed_indentation + +Metrics/LineLength: + Max: 120 + +Metrics/BlockLength: + Enabled: false + +Layout/EndAlignment: + EnforcedStyleAlignWith: variable + SupportedStylesAlignWith: + - keyword + - variable + +Lint/AmbiguousBlockAssociation: + Exclude: + - spec/**/* diff --git a/.ruby-version b/.ruby-version index e75da3e..73462a5 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.3.6 +2.5.1 diff --git a/Gemfile b/Gemfile index f214d12..74bf1d1 100644 --- a/Gemfile +++ b/Gemfile @@ -1,60 +1,56 @@ source "https://rubygems.org" -ruby "2.3.6" +ruby "2.5.1" # the most important stuff -gem "rails", "4.2.8" gem "pg" -gem "rails-api" -gem "rails_api_format", git: "https://github.com/fs/rails-api-format.git" +gem "rails", "5.2.1" # all other gems -gem "active_model_serializers", git: "https://github.com/rails-api/active_model_serializers.git" +gem "active_model_serializers" +gem "bcrypt" +gem "bootsnap", require: false gem "decent_exposure" -gem "devise" gem "health_check" gem "interactor" gem "kaminari" -gem "rack-cors", require: "rack/cors" +gem "knock" +gem "puma" +gem "rack-cors" gem "responders" gem "rollbar" gem "seedbank" -gem "simple_token_authentication" -gem "thin" group :development do - gem "letter_opener" - gem "foreman" gem "bullet" - + gem "letter_opener" + gem "rails-erd" gem "spring" gem "spring-commands-rspec" + gem "spring-watcher-listen" end group :development, :test do + gem "brakeman" + gem "bundler-audit" gem "byebug" gem "dotenv-rails" + gem "rspec-its" gem "rspec-rails" - gem "mail_safe" - - gem "brakeman" gem "rubocop" - gem "bundler-audit" + gem "rubocop-rspec" end group :test do - gem "simplecov", require: false - gem "webmock", require: false - gem "database_cleaner" gem "email_spec" + gem "json_matchers" + gem "rspec_api_documentation" gem "shoulda-matchers", require: false - gem "json_spec" + gem "webmock", require: false end group :development, :test, :staging do + gem "factory_bot_rails" gem "faker" - gem "factory_girl_rails" - gem "rspec_api_documentation" - gem "apitome" end diff --git a/Gemfile.lock b/Gemfile.lock index 06b91fb..21d5740 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,331 +1,320 @@ -GIT - remote: https://github.com/fs/rails-api-format.git - revision: c3acffa0fb559c9bb9fa1f615ab60acc256f3c0b - specs: - rails_api_format (0.0.2) - active_model_serializers (~> 0.10.0.rc2) - devise (~> 3.5.1) - rails (~> 4.2.3) - responders (~> 2.1.0) - -GIT - remote: https://github.com/rails-api/active_model_serializers.git - revision: 6aba26049154712d65176708ffad55fcfe512ca0 - specs: - active_model_serializers (0.10.0.rc2) - activemodel (>= 4.0) - GEM remote: https://rubygems.org/ specs: - actionmailer (4.2.8) - actionpack (= 4.2.8) - actionview (= 4.2.8) - activejob (= 4.2.8) + actioncable (5.2.1) + actionpack (= 5.2.1) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + actionmailer (5.2.1) + actionpack (= 5.2.1) + actionview (= 5.2.1) + activejob (= 5.2.1) mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 1.0, >= 1.0.5) - actionpack (4.2.8) - actionview (= 4.2.8) - activesupport (= 4.2.8) - rack (~> 1.6) - rack-test (~> 0.6.2) - rails-dom-testing (~> 1.0, >= 1.0.5) + rails-dom-testing (~> 2.0) + actionpack (5.2.1) + actionview (= 5.2.1) + activesupport (= 5.2.1) + rack (~> 2.0) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (4.2.8) - activesupport (= 4.2.8) + actionview (5.2.1) + activesupport (= 5.2.1) builder (~> 3.1) - erubis (~> 2.7.0) - rails-dom-testing (~> 1.0, >= 1.0.5) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (4.2.8) - activesupport (= 4.2.8) - globalid (>= 0.3.0) - activemodel (4.2.8) - activesupport (= 4.2.8) - builder (~> 3.1) - activerecord (4.2.8) - activemodel (= 4.2.8) - activesupport (= 4.2.8) - arel (~> 6.0) - activesupport (4.2.8) - i18n (~> 0.7) + active_model_serializers (0.10.7) + actionpack (>= 4.1, < 6) + activemodel (>= 4.1, < 6) + case_transform (>= 0.2) + jsonapi-renderer (>= 0.1.1.beta1, < 0.3) + activejob (5.2.1) + activesupport (= 5.2.1) + globalid (>= 0.3.6) + activemodel (5.2.1) + activesupport (= 5.2.1) + activerecord (5.2.1) + activemodel (= 5.2.1) + activesupport (= 5.2.1) + arel (>= 9.0) + activestorage (5.2.1) + actionpack (= 5.2.1) + activerecord (= 5.2.1) + marcel (~> 0.3.1) + activesupport (5.2.1) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 0.7, < 2) minitest (~> 5.1) - thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) - addressable (2.3.6) - apitome (0.0.7) - github-markdown - railties (>= 3.2.5, < 5) - rspec_api_documentation - arel (6.0.4) + addressable (2.5.2) + public_suffix (>= 2.0.2, < 4.0) + arel (9.0.0) ast (2.4.0) - bcrypt (3.1.10) - brakeman (2.4.3) - erubis (~> 2.6) - fastercsv (~> 1.5) - haml (>= 3.0, < 5.0) - highline (~> 1.6.20) - multi_json (~> 1.2) - ruby2ruby (~> 2.0.5) - ruby_parser (~> 3.4.0) - sass (~> 3.0) - slim (>= 1.3.6, < 3.0) - terminal-table (~> 1.4) + bcrypt (3.1.12) + bootsnap (1.3.1) + msgpack (~> 1.0) + brakeman (4.3.1) builder (3.2.3) - bullet (4.14.7) + bullet (5.7.6) activesupport (>= 3.0.0) - uniform_notifier (~> 1.9.0) - bundler-audit (0.3.1) + uniform_notifier (~> 1.11.0) + bundler-audit (0.6.0) bundler (~> 1.2) thor (~> 0.18) - byebug (6.0.2) + byebug (10.0.2) + case_transform (0.2) + activesupport + choice (0.2.0) concurrent-ruby (1.0.5) - crack (0.4.2) + crack (0.4.3) safe_yaml (~> 1.0.0) - crass (1.0.3) - daemons (1.1.9) - database_cleaner (1.2.0) - decent_exposure (3.0.0) + crass (1.0.4) + database_cleaner (1.7.0) + decent_exposure (3.0.2) activesupport (>= 4.0) - devise (3.5.6) - bcrypt (~> 3.0) - orm_adapter (~> 0.1) - railties (>= 3.2.6, < 5) - responders - thread_safe (~> 0.1) - warden (~> 1.2.3) - diff-lcs (1.2.5) - docile (1.1.3) - dotenv (0.10.0) - dotenv-rails (0.10.0) - dotenv (= 0.10.0) - email_spec (1.5.0) + diff-lcs (1.3) + dotenv (2.5.0) + dotenv-rails (2.5.0) + dotenv (= 2.5.0) + railties (>= 3.2, < 6.0) + email_spec (2.2.0) + htmlentities (~> 4.3.3) launchy (~> 2.1) - mail (~> 2.2) - erubis (2.7.0) - eventmachine (1.0.7) - factory_girl (4.4.0) + mail (~> 2.7) + erubi (1.7.1) + factory_bot (4.11.0) activesupport (>= 3.0.0) - factory_girl_rails (4.4.1) - factory_girl (~> 4.4.0) + factory_bot_rails (4.11.0) + factory_bot (~> 4.11.0) railties (>= 3.0.0) - faker (1.6.6) - i18n (~> 0.5) - fastercsv (1.5.5) - foreman (0.63.0) - dotenv (>= 0.7) - thor (>= 0.13.6) - github-markdown (0.6.6) - globalid (0.3.7) + faker (1.9.1) + i18n (>= 0.7) + ffi (1.9.25) + globalid (0.4.1) + activesupport (>= 4.2.0) + hashdiff (0.3.7) + health_check (3.0.0) + railties (>= 5.0) + htmlentities (4.3.4) + i18n (1.1.0) + concurrent-ruby (~> 1.0) + interactor (3.1.1) + jaro_winkler (1.5.1) + json_matchers (0.10.0) + json_schema + json_schema (0.19.1) + jsonapi-renderer (0.2.0) + jwt (1.5.6) + kaminari (1.1.1) activesupport (>= 4.1.0) - haml (4.0.5) - tilt - health_check (2.2.1) - rails (>= 4.0) - highline (1.6.21) - i18n (0.8.1) - interactor (3.1.0) - json (1.8.6) - json_spec (1.1.4) - multi_json (~> 1.0) - rspec (>= 2.0, < 4.0) - kaminari (0.16.1) - actionpack (>= 3.0.0) - activesupport (>= 3.0.0) - launchy (2.4.2) + kaminari-actionview (= 1.1.1) + kaminari-activerecord (= 1.1.1) + kaminari-core (= 1.1.1) + kaminari-actionview (1.1.1) + actionview + kaminari-core (= 1.1.1) + kaminari-activerecord (1.1.1) + activerecord + kaminari-core (= 1.1.1) + kaminari-core (1.1.1) + knock (2.1.1) + bcrypt (~> 3.1) + jwt (~> 1.5) + rails (>= 4.2) + launchy (2.4.3) addressable (~> 2.3) - letter_opener (1.2.0) + letter_opener (1.6.0) launchy (~> 2.2) + listen (3.1.5) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + ruby_dep (~> 1.2) loofah (2.2.2) crass (~> 1.0.2) nokogiri (>= 1.5.9) - mail (2.6.4) - mime-types (>= 1.16, < 4) - mail_safe (0.3.1) - actionmailer (>= 1.3.6) - mime-types (3.1) - mime-types-data (~> 3.2015) - mime-types-data (3.2016.0521) + mail (2.7.0) + mini_mime (>= 0.1.1) + marcel (0.3.2) + mimemagic (~> 0.3.2) + method_source (0.9.0) + mimemagic (0.3.2) + mini_mime (1.0.1) mini_portile2 (2.3.0) - minitest (5.10.1) - multi_json (1.12.1) - mustache (0.99.8) + minitest (5.11.3) + msgpack (1.2.4) + multi_json (1.13.1) + mustache (1.0.5) + nio4r (2.3.1) nokogiri (1.8.4) mini_portile2 (~> 2.3.0) - orm_adapter (0.5.0) parallel (1.12.1) - parser (2.5.0.5) + parser (2.5.1.2) ast (~> 2.4.0) - pg (0.17.1) - powerpack (0.1.1) - rack (1.6.10) + pg (1.1.3) + powerpack (0.1.2) + public_suffix (3.0.3) + puma (3.12.0) + rack (2.0.5) rack-cors (1.0.2) - rack-test (0.6.3) - rack (>= 1.0) - rails (4.2.8) - actionmailer (= 4.2.8) - actionpack (= 4.2.8) - actionview (= 4.2.8) - activejob (= 4.2.8) - activemodel (= 4.2.8) - activerecord (= 4.2.8) - activesupport (= 4.2.8) - bundler (>= 1.3.0, < 2.0) - railties (= 4.2.8) - sprockets-rails - rails-api (0.4.1) - actionpack (>= 3.2.11) - railties (>= 3.2.11) - rails-deprecated_sanitizer (1.0.3) - activesupport (>= 4.2.0.alpha) - rails-dom-testing (1.0.8) - activesupport (>= 4.2.0.beta, < 5.0) - nokogiri (~> 1.6) - rails-deprecated_sanitizer (>= 1.0.1) + rack-test (1.1.0) + rack (>= 1.0, < 3) + rails (5.2.1) + actioncable (= 5.2.1) + actionmailer (= 5.2.1) + actionpack (= 5.2.1) + actionview (= 5.2.1) + activejob (= 5.2.1) + activemodel (= 5.2.1) + activerecord (= 5.2.1) + activestorage (= 5.2.1) + activesupport (= 5.2.1) + bundler (>= 1.3.0) + railties (= 5.2.1) + sprockets-rails (>= 2.0.0) + rails-dom-testing (2.0.3) + activesupport (>= 4.2.0) + nokogiri (>= 1.6) + rails-erd (1.5.2) + activerecord (>= 3.2) + activesupport (>= 3.2) + choice (~> 0.2.0) + ruby-graphviz (~> 1.2) rails-html-sanitizer (1.0.4) loofah (~> 2.2, >= 2.2.2) - railties (4.2.8) - actionpack (= 4.2.8) - activesupport (= 4.2.8) + railties (5.2.1) + actionpack (= 5.2.1) + activesupport (= 5.2.1) + method_source rake (>= 0.8.7) - thor (>= 0.18.1, < 2.0) + thor (>= 0.19.0, < 2.0) rainbow (3.0.0) - rake (11.3.0) - responders (2.1.1) - railties (>= 4.2.0, < 5.1) - rollbar (2.12.0) + rake (12.3.1) + rb-fsevent (0.10.3) + rb-inotify (0.9.10) + ffi (>= 0.5.0, < 2) + responders (2.4.0) + actionpack (>= 4.2.0, < 5.3) + railties (>= 4.2.0, < 5.3) + rollbar (2.17.0) multi_json - rspec (3.3.0) - rspec-core (~> 3.3.0) - rspec-expectations (~> 3.3.0) - rspec-mocks (~> 3.3.0) - rspec-core (3.3.2) - rspec-support (~> 3.3.0) - rspec-expectations (3.3.1) + rspec (3.8.0) + rspec-core (~> 3.8.0) + rspec-expectations (~> 3.8.0) + rspec-mocks (~> 3.8.0) + rspec-core (3.8.0) + rspec-support (~> 3.8.0) + rspec-expectations (3.8.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.3.0) - rspec-mocks (3.3.2) + rspec-support (~> 3.8.0) + rspec-its (1.2.0) + rspec-core (>= 3.0.0) + rspec-expectations (>= 3.0.0) + rspec-mocks (3.8.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.3.0) - rspec-rails (3.3.3) - actionpack (>= 3.0, < 4.3) - activesupport (>= 3.0, < 4.3) - railties (>= 3.0, < 4.3) - rspec-core (~> 3.3.0) - rspec-expectations (~> 3.3.0) - rspec-mocks (~> 3.3.0) - rspec-support (~> 3.3.0) - rspec-support (3.3.0) - rspec_api_documentation (4.4.0) + rspec-support (~> 3.8.0) + rspec-rails (3.8.0) + actionpack (>= 3.0) + activesupport (>= 3.0) + railties (>= 3.0) + rspec-core (~> 3.8.0) + rspec-expectations (~> 3.8.0) + rspec-mocks (~> 3.8.0) + rspec-support (~> 3.8.0) + rspec-support (3.8.0) + rspec_api_documentation (6.0.0) activesupport (>= 3.0.0) - json (~> 1.4, >= 1.4.6) - mustache (~> 0.99, >= 0.99.4) - rspec (>= 3.0.0) - rubocop (0.54.0) + mustache (~> 1.0, >= 0.99.4) + rspec (~> 3.0) + rubocop (0.58.2) + jaro_winkler (~> 1.5.1) parallel (~> 1.10) - parser (>= 2.5) + parser (>= 2.5, != 2.5.1.1) powerpack (~> 0.1) rainbow (>= 2.2.2, < 4.0) ruby-progressbar (~> 1.7) unicode-display_width (~> 1.0, >= 1.0.1) - ruby-progressbar (1.7.5) - ruby2ruby (2.0.8) - ruby_parser (~> 3.1) - sexp_processor (~> 4.0) - ruby_parser (3.4.1) - sexp_processor (~> 4.1) - safe_yaml (1.0.2) - sass (3.3.4) - seedbank (0.3.0) - sexp_processor (4.4.3) - shoulda-matchers (2.8.0) - activesupport (>= 3.0.0) - simple_token_authentication (1.10.0) - actionmailer (>= 3.2.6, < 5) - actionpack (>= 3.2.6, < 5) - devise (~> 3.2) - simplecov (0.8.2) - docile (~> 1.1.0) - multi_json - simplecov-html (~> 0.8.0) - simplecov-html (0.8.0) - slim (2.0.2) - temple (~> 0.6.6) - tilt (>= 1.3.3, < 2.1) - spring (1.3.6) + rubocop-rspec (1.29.1) + rubocop (>= 0.58.0) + ruby-graphviz (1.2.3) + ruby-progressbar (1.10.0) + ruby_dep (1.5.0) + safe_yaml (1.0.4) + seedbank (0.4.0) + shoulda-matchers (3.1.2) + activesupport (>= 4.0.0) + spring (2.0.2) + activesupport (>= 4.2) spring-commands-rspec (1.0.4) spring (>= 0.9.1) + spring-watcher-listen (2.0.1) + listen (>= 2.7, < 4.0) + spring (>= 1.2, < 3.0) sprockets (3.7.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-rails (3.2.0) + sprockets-rails (3.2.1) actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) - temple (0.6.7) - terminal-table (1.4.5) - thin (1.6.2) - daemons (>= 1.0.9) - eventmachine (>= 1.0.0) - rack (>= 1.0.0) thor (0.19.4) thread_safe (0.3.6) - tilt (2.0.1) - tzinfo (1.2.2) + tzinfo (1.2.5) thread_safe (~> 0.1) - unicode-display_width (1.3.0) - uniform_notifier (1.9.0) - warden (1.2.6) - rack (>= 1.0) - webmock (1.17.4) - addressable (>= 2.2.7) + unicode-display_width (1.4.0) + uniform_notifier (1.11.0) + webmock (3.4.2) + addressable (>= 2.3.6) crack (>= 0.3.2) + hashdiff + websocket-driver (0.7.0) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.3) PLATFORMS ruby DEPENDENCIES - active_model_serializers! - apitome + active_model_serializers + bcrypt + bootsnap brakeman bullet bundler-audit byebug database_cleaner decent_exposure - devise dotenv-rails email_spec - factory_girl_rails + factory_bot_rails faker - foreman health_check interactor - json_spec + json_matchers kaminari + knock letter_opener - mail_safe pg + puma rack-cors - rails (= 4.2.8) - rails-api - rails_api_format! + rails (= 5.2.1) + rails-erd responders rollbar + rspec-its rspec-rails rspec_api_documentation rubocop + rubocop-rspec seedbank shoulda-matchers - simple_token_authentication - simplecov spring spring-commands-rspec - thin + spring-watcher-listen webmock RUBY VERSION - ruby 2.3.6p384 + ruby 2.5.1p57 BUNDLED WITH - 1.16.3 + 1.16.4 diff --git a/Procfile b/Procfile index ca35301..c2c566e 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -web: bundle exec rails server thin -p $PORT -e $RACK_ENV +web: bundle exec puma -C config/puma.rb diff --git a/README.md b/README.md index e137cb4..57ca3a2 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,51 @@ -# Skeleton for new Rails 4 application for REST API +# Skeleton for new Rails 5 application for JSON API -[](https://codeclimate.com/github/fs/rails-base-api) [](https://semaphoreci.com/fs/rails-base-api) [](https://snyk.io/test/github/fs/rails-base-api) -This simple application includes ruby/rails technology which we use at FlatStack for new REST API projects. - -Application currently based on Rails 4 stable branch and Ruby 2.3.6 - -## API - -Status of the API could be checked at [http://localhost:5000/docs](http://localhost:5000/docs) +This simple application includes Ruby/Rails technology which we use at Flatstack for new [JSON API](http://jsonapi.org) projects. Application currently based on Rails 5 stable branch and Ruby 2.5.1 ## What's included ### Application gems: -* [Decent Exposure](https://github.com/voxdolo/decent_exposure) for DRY controllers -* [Rollbar](https://github.com/rollbar/rollbar-gem) for exception notification -* [Thin](https://github.com/macournoyer/thin) as rails web server -* [Kaminari](https://github.com/amatsuda/kaminari) for pagination -* [Rack CORS](https://github.com/cyu/rack-cors) for [CORS](http://en.wikipedia.org/wiki/Cross-origin_resource_sharing) +* [active_model_serializers](https://github.com/rails-api/active_model_serializers) - resource serializers for JSON API +* [decent_exposure](https://github.com/voxdolo/decent_exposure) for DRY controllers +* [health_check](https://github.com/ianheggie/health_check) - health check endpoint +* [interactor](https://github.com/collectiveidea/interactor) – encapsulates business logic +* [kaminari](https://github.com/amatsuda/kaminari) for pagination +* [knock](https://github.com/nsarno/knock) – seamless JWT authentication +* [puma](https://github.com/puma/puma) as Rails web server +* [rack-cors](https://github.com/cyu/rack-cors) for [CORS](http://en.wikipedia.org/wiki/Cross-origin_resource_sharing) +* [responders](https://github.com/plataformatec/responders) - DRY controllers +* [rollbar](https://github.com/rollbar/rollbar-gem) for exception notification ### Development gems -* [Foreman](https://github.com/ddollar/foreman) for managing development stack with Procfile -* [Letter Opener](https://github.com/ryanb/letter_opener) for preview mail in the browser instead of sending -* [Mail Safe](https://github.com/myronmarston/mail_safe) keep ActionMailer emails from escaping into the wild during development -* [Bullet](https://github.com/flyerhzm/bullet) gem to kill N+1 queries and unused eager loading -* [Rails Best Practices](https://github.com/railsbp/rails_best_practices) code metric tool -* [Brakeman](https://github.com/presidentbeef/brakeman) static analysis security vulnerability scanner -* [Bundler Audit](https://github.com/rubysec/bundler-audit) Patch-level verification for Gems -* [Spring](https://github.com/rails/spring) for fast Rails actions via pre-loading +* [brakeman](https://github.com/presidentbeef/brakeman) - static analysis security vulnerability scanner +* [bullet](https://github.com/flyerhzm/bullet) - kill n+1 queries and unused eager loading +* [bundler-audit](https://github.com/rubysec/bundler-audit) - patch-level verification for gems +* [dotenv](https://github.com/bkeepers/dotenv) - load environment variables from `.env` +* [letter_opener](https://github.com/ryanb/letter_opener) - preview E-Mails in the browser instead of sending +* [rails-erd](https://github.com/voormedia/rails-erd) - generate a diagram based on application's AR models +* [seedbank](https://github.com/james2m/seedbank) - seeds on steroids ### Testing gems -* [Factory Girl](https://github.com/thoughtbot/factory_girl) for easier creation of test data -* [RSpec](https://github.com/rspec/rspec) for awesome, readable isolation testing -* [Shoulda Matchers](http://github.com/thoughtbot/shoulda-matchers) for frequently needed Rails and RSpec matchers -* [Email Spec](https://github.com/bmabey/email-spec) Collection of rspec matchers and cucumber steps for testing emails -* [Rspec Api Documentation](https://github.com/zipmark/rspec_api_documentation) Generate pretty API docs for your Rails APIs +* [factory bot](https://github.com/thoughtbot/factory_bot) - create test data +* [faker](https://github.com/stympy/faker) - generate fake data +* [json_matchers](https://github.com/thoughtbot/json_matchers) - validate JSON with JSON Schema +* [rspec api documentation](https://github.com/zipmark/rspec_api_documentation) - generate pretty API docs +* [rspec](https://github.com/rspec/rspec) - awesome, readable isolation testing +* [shoulda matchers](http://github.com/thoughtbot/shoulda-matchers) - frequently needed Rails and RSpec matchers -### Initializes +### Non standard initializes -* `01_config.rb` - shortcut for getting application config with `app_config` -* `mailer.rb` - setup default hosts for mailer from configuration -* `requires.rb` - automatically requires everything in lib/ & lib/extensions -* `rack_cors.rb` - setup whitelist of domains to allow cross-origin resource sharing +* `active_model_serializer.rb` - setup serializers for JSON API +* `bullet.rb` - setup Bullet to catch up N+1 +* `cors.rb` - setup whitelist of domains to allow cross-origin resource sharing +* `health_check.rb` - setup Health Check endpoint +* `rollbar.rb` - setup Rollbar ### Scripts @@ -55,39 +54,6 @@ Status of the API could be checked at [http://localhost:5000/docs](http://localh * `bin/ci` - should be used in the CI or locally * `bin/server` - to run server locally -### Serializers - -### PaginatedArraySerializer - -Use that serializer if you want to add meta with pagination info on response - -```ruby -def index - respond_with( - posts, - serializer: PaginatedArraySerializer - ) -end -``` - -The above usage of `PaginatedArraySerializer` will produce the following: - -```json -{ - "meta": { - "pagination": { - "total":46, - "per_page":2, - "page":1 - } - }, - "posts": [ - { "title": "Post 1", "body": "Hello!" }, - { "title": "Post 2", "body": "Goodbye!" } - ] -} -``` - ## Quick start Clone application as new project with original repository named "rails-base-api" @@ -130,10 +96,6 @@ mv doc/README_TEMPLATE.md README.md git commit -am "Update README.md" ``` -### Restrict access to documentation - -You can enable restrict access to documentation by adding `APITOME_USER` and `APITOME_PASSWORD` environment variables. Can be useful on staging environment. - ## Examples Please check how to build API endpoints and test them properly in the diff --git a/Rakefile b/Rakefile index 041dae9..99e6b8a 100644 --- a/Rakefile +++ b/Rakefile @@ -1,4 +1,3 @@ -#!/usr/bin/env rake # 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. diff --git a/app.json b/app.json index c716306..c28419c 100644 --- a/app.json +++ b/app.json @@ -1,12 +1,17 @@ { "name": "Rails Base API", - "description": "Skeleton for new Rails 4 application for REST API", + "description": "Skeleton for new Rails application with JSON API", "env": { - "ALLOW_REQUESTS_FROM": { "required": true }, + "HOST": { "required": true }, + "LANG": { "required": true }, + "CORS_ORIGINS": { "required": true }, "RACK_ENV": { "required": true }, - "SECRET_KEY_BASE": { "required": true } + "RAILS_ENV": { "required": true }, + "SECRET_KEY_BASE": { "required": true }, + "MAILER_SENDER_ADDRESS": { "required": true } }, "addons": [ - "heroku-postgresql:hobby-dev" + "heroku-postgresql", + "sendgrid" ] } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb deleted file mode 100644 index fcc34a2..0000000 --- a/app/controllers/application_controller.rb +++ /dev/null @@ -1,5 +0,0 @@ -class ApplicationController < ActionController::API - include ActionController::ImplicitRender - - respond_to :json -end diff --git a/app/controllers/v1/base_controller.rb b/app/controllers/v1/base_controller.rb new file mode 100644 index 0000000..672e8d8 --- /dev/null +++ b/app/controllers/v1/base_controller.rb @@ -0,0 +1,43 @@ +module V1 + class BaseController < ActionController::API + include Knock::Authenticable + + before_action :authenticate_user! + + rescue_from ActiveRecord::RecordNotFound do |_exception| + respond_with_error(:record_not_found) + end + + private + + def authenticate_user! + respond_with_error(:unauthorized) if current_user.blank? + end + + def current_user + @current_user ||= token && authenticate_for(User) + end + + def respond_with_resource(resource, status: :ok, location: resource, include: nil, fields: nil) + render jsonapi: resource, include: include, status: status, location: location, fields: fields + end + + def respond_with_resources(resources, include: nil, fields: nil) + respond_with_resource(resources, include: include, location: nil, fields: fields) + end + + def respond_with_resource_errors(resource, status: :unprocessable_entity) + render jsonapi: resource, serializer: ActiveModel::Serializer::ErrorSerializer, status: status + end + + def respond_with_error(code) + Error.new(code: code).tap do |error| + render json: error.to_json, status: error.status + end + end + + def jsonapi_params(options) + ActiveModelSerializers::Deserialization.jsonapi_parse!(params, options) + end + end +end diff --git a/app/controllers/v1/profiles_controller.rb b/app/controllers/v1/profiles_controller.rb new file mode 100644 index 0000000..9b5bfb7 --- /dev/null +++ b/app/controllers/v1/profiles_controller.rb @@ -0,0 +1,26 @@ +module V1 + class ProfilesController < V1::BaseController + def show + respond_with_resource(current_user, location: :v1_profile) + end + + def update + if current_user.update(user_params) + respond_with_resource(current_user, status: :ok, location: :v1_profile) + else + respond_with_resource_errors(current_user) + end + end + + def destroy + current_user.destroy + respond_with_resource(current_user, location: nil) + end + + private + + def user_params + jsonapi_params(only: %i[full_name email password]) + end + end +end diff --git a/app/controllers/v1/registrations_controller.rb b/app/controllers/v1/registrations_controller.rb new file mode 100644 index 0000000..ff35569 --- /dev/null +++ b/app/controllers/v1/registrations_controller.rb @@ -0,0 +1,21 @@ +module V1 + class RegistrationsController < V1::BaseController + skip_before_action :authenticate_user! + + expose :user + + def create + if user.save + respond_with_resource(user, status: :created, location: :v1_profile) + else + respond_with_resource_errors(user) + end + end + + private + + def user_params + jsonapi_params(only: %i[full_name email password]) + end + end +end diff --git a/app/controllers/v1/sessions_controller.rb b/app/controllers/v1/sessions_controller.rb deleted file mode 100644 index e4657df..0000000 --- a/app/controllers/v1/sessions_controller.rb +++ /dev/null @@ -1,10 +0,0 @@ -module V1 - class SessionsController < Devise::SessionsController - wrap_parameters :user - - def create - user = AuthenticateUser.call(warden: warden).user - respond_with(user, serializer: SessionSerializer) - end - end -end diff --git a/app/controllers/v1/tokens_controller.rb b/app/controllers/v1/tokens_controller.rb new file mode 100644 index 0000000..dc869b0 --- /dev/null +++ b/app/controllers/v1/tokens_controller.rb @@ -0,0 +1,21 @@ +module V1 + class TokensController < V1::BaseController + skip_before_action :authenticate_user! + + def create + result = CreateJwt.call(authentication_params) + + if result.success? + respond_with_resource(result.jwt_token, status: :created, location: nil) + else + respond_with_error(result.code) + end + end + + private + + def authentication_params + jsonapi_params(only: %i[email password]) + end + end +end diff --git a/app/controllers/v1/users_controller.rb b/app/controllers/v1/users_controller.rb new file mode 100644 index 0000000..87657d7 --- /dev/null +++ b/app/controllers/v1/users_controller.rb @@ -0,0 +1,14 @@ +module V1 + class UsersController < V1::BaseController + expose :user + expose :users, -> { User.all } + + def index + respond_with_resources(users) + end + + def show + respond_with_resource(user, location: :v1_user) + end + end +end diff --git a/app/interactors/authenticate_user.rb b/app/interactors/authenticate_user.rb deleted file mode 100644 index b404b1d..0000000 --- a/app/interactors/authenticate_user.rb +++ /dev/null @@ -1,16 +0,0 @@ -class AuthenticateUser - include Interactor - - OPTIONS = { store: false, scope: :user }.freeze - - def call - context.user = authenticated_user! - end - - private - - def authenticated_user! - context.warden.request.env["devise.skip_trackable"] = false - context.warden.authenticate!(OPTIONS) - end -end diff --git a/app/interactors/create_jwt.rb b/app/interactors/create_jwt.rb new file mode 100644 index 0000000..055c0b0 --- /dev/null +++ b/app/interactors/create_jwt.rb @@ -0,0 +1,24 @@ +class CreateJwt + include Interactor + + delegate :email, :password, to: :context + + def call + context.fail!(code: :invalid_credentials) unless authenticated? + context.jwt_token = jwt_token + end + + private + + def authenticated? + user.present? && user.authenticate(password) + end + + def jwt_token + JwtToken.new(payload: { sub: user.id }) + end + + def user + @user ||= User.find_by(email: email) + end +end diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 0000000..10a4cba --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end diff --git a/app/models/error.rb b/app/models/error.rb new file mode 100644 index 0000000..267dd54 --- /dev/null +++ b/app/models/error.rb @@ -0,0 +1,35 @@ +require "securerandom" + +class Error + include ActiveModel::Model + include ActiveModel::Serialization + + CODES_TO_STATUS = { + invalid_credentials: :unprocessable_entity, + unauthorized: :unauthorized, + record_not_found: :not_found, + route_not_found: :not_found, + custom_error: :internal_server_error + }.freeze + + attr_accessor :code + + def attributes + { + code: code, + detail: detail + } + end + + def detail + I18n.t("errors.#{code}") + end + + def status + CODES_TO_STATUS[code] + end + + def to_json + ActiveModelSerializers::SerializableResource.new([self], adapter: :json).to_json + end +end diff --git a/app/models/jwt_token.rb b/app/models/jwt_token.rb new file mode 100644 index 0000000..0ec7894 --- /dev/null +++ b/app/models/jwt_token.rb @@ -0,0 +1,7 @@ +class JwtToken < Knock::AuthToken + include ActiveModel::Serialization + + def id + token + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 6e1e916..16282c3 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,6 +1,7 @@ -class User < ActiveRecord::Base - acts_as_token_authenticatable +class User < ApplicationRecord + has_secure_password - devise :database_authenticatable, :registerable, - :recoverable, :trackable, :validatable + validates :email, presence: true + validates :password, length: { minimum: 6 } + validates :email, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP } end diff --git a/app/serializers/application_serializer.rb b/app/serializers/application_serializer.rb index cfeb424..5e940b5 100644 --- a/app/serializers/application_serializer.rb +++ b/app/serializers/application_serializer.rb @@ -1,2 +1,5 @@ class ApplicationSerializer < ActiveModel::Serializer + def self.decorate_collection(collection) + ActiveModel::Serializer::CollectionSerializer.new(collection) + end end diff --git a/app/serializers/error_serializer.rb b/app/serializers/error_serializer.rb new file mode 100644 index 0000000..1cd490b --- /dev/null +++ b/app/serializers/error_serializer.rb @@ -0,0 +1,3 @@ +class ErrorSerializer < ActiveModel::Serializer::ErrorSerializer + attributes :code, :detail +end diff --git a/app/serializers/jwt_token_serializer.rb b/app/serializers/jwt_token_serializer.rb new file mode 100644 index 0000000..9ea7a9c --- /dev/null +++ b/app/serializers/jwt_token_serializer.rb @@ -0,0 +1,3 @@ +class JwtTokenSerializer < ApplicationSerializer + attributes :token +end diff --git a/app/serializers/paginated_array_serializer.rb b/app/serializers/paginated_array_serializer.rb deleted file mode 100644 index c183d22..0000000 --- a/app/serializers/paginated_array_serializer.rb +++ /dev/null @@ -1,12 +0,0 @@ -class PaginatedArraySerializer < ActiveModel::Serializer::ArraySerializer - def initialize(objects, options = {}) - options[:meta] ||= {} - options[:meta][:pagination] = { - total: objects.total_count, - per_page: objects.limit_value, - page: objects.current_page - } - - super(objects, options) - end -end diff --git a/app/serializers/session_serializer.rb b/app/serializers/session_serializer.rb deleted file mode 100644 index 42d78ce..0000000 --- a/app/serializers/session_serializer.rb +++ /dev/null @@ -1,3 +0,0 @@ -class SessionSerializer < UserSerializer - attributes :authentication_token -end diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb index 4e58950..8b2302a 100644 --- a/app/serializers/user_serializer.rb +++ b/app/serializers/user_serializer.rb @@ -1,3 +1,3 @@ class UserSerializer < ApplicationSerializer - attributes :id, :email + attributes :email, :full_name end diff --git a/bin/bundle b/bin/bundle index 66e9889..f19acf5 100755 --- a/bin/bundle +++ b/bin/bundle @@ -1,3 +1,3 @@ #!/usr/bin/env ruby -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) load Gem.bin_path('bundler', 'bundle') diff --git a/bin/doc b/bin/doc new file mode 100755 index 0000000..7596121 --- /dev/null +++ b/bin/doc @@ -0,0 +1,6 @@ +#!/usr/bin/env sh + +set -e + +bin/rspec spec/api/ --format RspecApiDocumentation::ApiFormatter +bin/rails erd diff --git a/bin/quality b/bin/quality index 2b7c8c6..2abcac7 100755 --- a/bin/quality +++ b/bin/quality @@ -2,8 +2,7 @@ set -e +bin/rubocop bin/brakeman --quiet --skip-libs --exit-on-warn -bin/rubocop --config config/rubocop.yml - bin/bundle-audit update bin/bundle-audit diff --git a/bin/rails b/bin/rails index 7feb6a3..5badb2f 100755 --- a/bin/rails +++ b/bin/rails @@ -1,8 +1,9 @@ #!/usr/bin/env ruby begin - load File.expand_path("../spring", __FILE__) -rescue LoadError + load File.expand_path('../spring', __FILE__) +rescue LoadError => e + raise unless e.message.include?('spring') end -APP_PATH = File.expand_path('../../config/application', __FILE__) +APP_PATH = File.expand_path('../config/application', __dir__) require_relative '../config/boot' require 'rails/commands' diff --git a/bin/rake b/bin/rake index 8017a02..d87d5f5 100755 --- a/bin/rake +++ b/bin/rake @@ -1,7 +1,8 @@ #!/usr/bin/env ruby begin - load File.expand_path("../spring", __FILE__) -rescue LoadError + load File.expand_path('../spring', __FILE__) +rescue LoadError => e + raise unless e.message.include?('spring') end require_relative '../config/boot' require 'rake' diff --git a/bin/rspec b/bin/rspec index 5318d0c..5a3c87c 100755 --- a/bin/rspec +++ b/bin/rspec @@ -1,7 +1,8 @@ #!/usr/bin/env ruby begin - load File.expand_path("../spring", __FILE__) -rescue LoadError + load File.expand_path('../spring', __FILE__) +rescue LoadError => e + raise unless e.message.include?('spring') end # # This file was generated by Bundler. diff --git a/bin/server b/bin/server index 495e9a4..df39613 100755 --- a/bin/server +++ b/bin/server @@ -1,3 +1,3 @@ #!/usr/bin/env sh -exec bin/foreman start +bin/rails server --port 5000 --binding lvh.me diff --git a/bin/setup b/bin/setup index a500856..1c58d77 100755 --- a/bin/setup +++ b/bin/setup @@ -22,4 +22,9 @@ if [ ! -f .env ]; then fi # Set up database and add any development seed data -bundle exec rake db:setup +bundle exec rails db:create +bundle exec rails db:schema:load + +if [ -z "$CI" ]; then + bundle exec rails db:seed +fi diff --git a/bin/spring b/bin/spring index 7b45d37..fb2ec2e 100755 --- a/bin/spring +++ b/bin/spring @@ -4,12 +4,14 @@ # It gets overwritten when you run the `spring binstub` command. unless defined?(Spring) - require "rubygems" - require "bundler" + require 'rubygems' + require 'bundler' - if match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m) - Gem.paths = { "GEM_PATH" => [Bundler.bundle_path.to_s, *Gem.path].uniq } - gem "spring", match[1] - require "spring/binstub" + 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/bin/update b/bin/update new file mode 100755 index 0000000..67d0d49 --- /dev/null +++ b/bin/update @@ -0,0 +1,28 @@ +#!/usr/bin/env ruby +require 'fileutils' +include FileUtils + +# path to your application root. +APP_ROOT = File.expand_path('..', __dir__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +chdir APP_ROOT do + # This script is a way to update your development environment automatically. + # Add necessary update steps to this file. + + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') + + puts "\n== Updating database ==" + system! 'bin/rails db:migrate' + + 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/yarn b/bin/yarn new file mode 100755 index 0000000..460dd56 --- /dev/null +++ b/bin/yarn @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby +APP_ROOT = File.expand_path('..', __dir__) +Dir.chdir(APP_ROOT) do + begin + exec "yarnpkg", *ARGV + rescue Errno::ENOENT + $stderr.puts "Yarn executable was not detected in the system." + $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" + exit 1 + end +end diff --git a/config/application.rb b/config/application.rb index afa0773..1c5d1c9 100644 --- a/config/application.rb +++ b/config/application.rb @@ -1,9 +1,16 @@ -require File.expand_path("boot", __dir__) +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_view/railtie" +# require "action_cable/engine" +# require "sprockets/railtie" # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. @@ -11,28 +18,34 @@ module RailsBaseApi class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults 5.2 + # Settings in config/environments/* take precedence over those specified here. - # Application configuration should go into files in config/initializers - # -- all .rb files in that directory are automatically loaded. + # Application configuration can go into files in config/initializers + # -- all .rb files in that directory are automatically loaded after loading + # the framework and any gems in your application. + + # Only loads a smaller set of middleware suitable for API only apps. + # Middleware like session, flash, cookies can be added back manually. + # Skip views, helpers and assets when generating a new resource. + config.api_only = true # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. - # config.time_zone = 'Central Time (US & Canada)' + # config.time_zone = "Central Time (US & Canada)" # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. - # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] + config.i18n.load_path += Dir[Rails.root.join("config", "locales", "**", "*.{rb,yml}")] # config.i18n.default_locale = :de - # Default e-mail address which will be shown in the "from" devise emails, initializers/devise.rb. - config.noreply = "noreply@fs-rails-base-api.heroku.com" - - # Default host for action mailer, initializers/mailer.rb - config.host = "localhost:5000" + # Enable deflate / gzip compression of controller-generated responses + config.middleware.use Rack::Deflater - config.serve_static_files = false + # Set default From address for all Mailers + config.action_mailer.default_options = { from: ENV.fetch("MAILER_SENDER_ADDRESS") } - # Disable default Rails headers which do not make sense in - # API-only project (X-Frame-Options, X-XSS-Protection, X-Content-Type-Options) - config.action_dispatch.default_headers = {} + # Set URL options to be able to use url_for helpers + config.action_mailer.default_url_options = { host: ENV.fetch("HOST") } end end diff --git a/config/boot.rb b/config/boot.rb index 12c0a73..988a5dd 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -1,4 +1,4 @@ -# Set up gems listed in the Gemfile. ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) -require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"]) +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..b808b8d --- /dev/null +++ b/config/cable.yml @@ -0,0 +1,10 @@ +development: + adapter: async + +test: + adapter: async + +production: + adapter: redis + url: redis://localhost:6379/1 + channel_prefix: rails_base_api_production diff --git a/config/environment.rb b/config/environment.rb index e4429b4..cac5315 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -1,5 +1,5 @@ # Load the Rails application. -require File.expand_path("application", __dir__) +require_relative "application" # Initialize the Rails application. Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb index 5375800..66cbe42 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -9,12 +9,30 @@ # Do not eager load code on boot. config.eager_load = false - # Show full error reports and disable caching. - config.consider_all_requests_local = true - config.action_controller.perform_caching = false + # Show full error reports. + config.consider_all_requests_local = true + + # Enable/disable caching. By default caching is disabled. + # Run rails dev:cache to toggle caching. + if Rails.root.join("tmp", "caching-dev.txt").exist? + config.action_controller.perform_caching = true + + 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 + + # Store uploaded files on the local file system (see config/storage.yml for options) + config.active_storage.service = :local # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false + config.action_mailer.perform_caching = false # Preview email in the browser instead of sending it. config.action_mailer.delivery_method = :letter_opener @@ -25,6 +43,13 @@ # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + # Raises error for missing translations # config.action_view.raise_on_missing_translations = true + + # Use an evented file watcher to asynchronously detect changes in source code, + # routes, locales, etc. This feature depends on the listen gem. + config.file_watcher = ActiveSupport::EventedFileUpdateChecker end diff --git a/config/environments/production.rb b/config/environments/production.rb index faed79e..02034e9 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -14,34 +14,66 @@ config.consider_all_requests_local = false config.action_controller.perform_caching = true - # Enable Rack::Cache to put a simple HTTP cache in front of your application - # Add `rack-cache` to your Gemfile before enabling this. - # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. - # config.action_dispatch.rack_cache = true + # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] + # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). + # config.require_master_key = true + + # Disable serving static files from the `/public` folder by default since + # Apache or NGINX already handles this. + config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present? + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + if ENV["ASSET_HOST"] + config.action_mailer.asset_host = ENV["ASSET_HOST"] + # config.action_controller.asset_host = ENV["ASSET_HOST"] + end # Specifies the header that your server uses for sending files. - # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache - # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx + # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache + # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX + + # Store uploaded files on the local file system (see config/storage.yml for options) + config.active_storage.service = :local # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. - # config.force_ssl = true + config.force_ssl = true - # Set to :debug to see everything in the log. - config.log_level = :info + # Use the lowest log level to ensure availability of diagnostic information + # when problems arise. + config.log_level = :debug # Prepend all log lines with the following tags. - # config.log_tags = [ :subdomain, :uuid ] - - # Use a different logger for distributed setups. - # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) + config.log_tags = [:request_id] # Use a different cache store in production. # config.cache_store = :mem_cache_store + # Use a real queuing backend for Active Job (and separate queues per environment) + # config.active_job.queue_adapter = :resque + # config.active_job.queue_name_prefix = "rails_base_api_#{Rails.env}" + + config.action_mailer.perform_caching = false + # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false + # Enable Email delivery via custom SMTP server or via SendGrid by default + if ENV["SMTP_USERNAME"] || ENV["SENDGRID_USERNAME"] + config.action_mailer.delivery_method = :smtp + + config.action_mailer.smtp_settings = { + authentication: :plain, + enable_starttls_auto: true, + openssl_verify_mode: ENV.fetch("SMTP_OPENSSL_VERIFY_MODE", nil), + address: ENV.fetch("SMTP_ADDRESS", "smtp.sendgrid.net"), + port: ENV.fetch("SMTP_PORT", 587), + domain: ENV.fetch("SMTP_DOMAIN", "heroku.com"), + user_name: ENV.fetch("SMTP_USERNAME") { ENV.fetch("SENDGRID_USERNAME") }, + password: ENV.fetch("SMTP_PASSWORD") { ENV.fetch("SENDGRID_PASSWORD") } + } + end + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). config.i18n.fallbacks = true @@ -49,12 +81,19 @@ # Send deprecation notices to registered listeners. config.active_support.deprecation = :notify - # Disable automatic flushing of the log to improve performance. - # config.autoflush_log = false - # Use default logging formatter so that PID and timestamp are not suppressed. config.log_formatter = ::Logger::Formatter.new + # Use a different logger for distributed setups. + # require "syslog/logger" + # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name") + + if ENV["RAILS_LOG_TO_STDOUT"].present? + logger = ActiveSupport::Logger.new(STDOUT) + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + end + # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false end diff --git a/config/environments/staging.rb b/config/environments/staging.rb index faed79e..dd14317 100644 --- a/config/environments/staging.rb +++ b/config/environments/staging.rb @@ -1,60 +1,5 @@ -Rails.application.configure do - # Settings specified here will take precedence over those in config/application.rb. - - # Code is not reloaded between requests. - config.cache_classes = true - - # Eager load code on boot. This eager loads most of Rails and - # your application in memory, allowing both threaded web servers - # and those relying on copy on write to perform better. - # Rake tasks automatically ignore this option for performance. - config.eager_load = true - - # Full error reports are disabled and caching is turned on. - config.consider_all_requests_local = false - config.action_controller.perform_caching = true - - # Enable Rack::Cache to put a simple HTTP cache in front of your application - # Add `rack-cache` to your Gemfile before enabling this. - # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. - # config.action_dispatch.rack_cache = true - - # Specifies the header that your server uses for sending files. - # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache - # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx - - # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. - # config.force_ssl = true - - # Set to :debug to see everything in the log. - config.log_level = :info - - # Prepend all log lines with the following tags. - # config.log_tags = [ :subdomain, :uuid ] +require_relative "production" - # Use a different logger for distributed setups. - # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) - - # Use a different cache store in production. - # config.cache_store = :mem_cache_store - - # Ignore bad email addresses and do not raise email delivery errors. - # Set this to true and configure the email server for immediate delivery to raise delivery errors. - # config.action_mailer.raise_delivery_errors = false - - # Enable locale fallbacks for I18n (makes lookups for any locale fall back to - # the I18n.default_locale when a translation cannot be found). - config.i18n.fallbacks = true - - # Send deprecation notices to registered listeners. - config.active_support.deprecation = :notify - - # Disable automatic flushing of the log to improve performance. - # config.autoflush_log = false - - # Use default logging formatter so that PID and timestamp are not suppressed. - config.log_formatter = ::Logger::Formatter.new - - # Do not dump schema after migrations. - config.active_record.dump_schema_after_migration = false +Rails.application.configure do + config.force_ssl = false end diff --git a/config/environments/test.rb b/config/environments/test.rb index a6c05de..17d1fb7 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -12,16 +12,27 @@ # preloads Rails for running tests, you may have to set it to true. config.eager_load = false + # Configure public file server for tests with Cache-Control for performance. + config.public_file_server.enabled = true + config.public_file_server.headers = { + "Cache-Control" => "public, max-age=#{1.hour.to_i}" + } + # Show full error reports and disable caching. - config.consider_all_requests_local = false + config.consider_all_requests_local = true config.action_controller.perform_caching = false # Raise exceptions instead of rendering exception templates. - config.action_dispatch.show_exceptions = true + config.action_dispatch.show_exceptions = false # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false + # Store uploaded files on the local file system in a temporary directory + config.active_storage.service = :test + + config.action_mailer.perform_caching = false + # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. diff --git a/config/initializers/01_config.rb b/config/initializers/01_config.rb deleted file mode 100644 index df16477..0000000 --- a/config/initializers/01_config.rb +++ /dev/null @@ -1,7 +0,0 @@ -# The shortcut for getting application config -# Application config should be stored in the config/application.rb, config/environments/*.rb -module Kernel - def app_config - Rails.application.config - end -end diff --git a/config/initializers/active_model_serializer.rb b/config/initializers/active_model_serializer.rb index 5c20a90..ed897a8 100644 --- a/config/initializers/active_model_serializer.rb +++ b/config/initializers/active_model_serializer.rb @@ -1,3 +1,6 @@ -ActiveModel::Serializer.configure do |config| - config.adapter = :json +ActiveModelSerializers.config.adapter = :json_api +ActiveModelSerializers.config.key_transform = :underscore + +ActiveSupport.on_load(:action_controller) do + require "active_model_serializers/register_jsonapi_renderer" end diff --git a/config/initializers/apitome.rb b/config/initializers/apitome.rb deleted file mode 100644 index e52d4b4..0000000 --- a/config/initializers/apitome.rb +++ /dev/null @@ -1,52 +0,0 @@ -Apitome.setup do |config| - # This determines where the Apitome routes will be mounted. Changing this to "/api/documentation" for instance would - # allow you to browse to http://localhost:3000/api/documentation to see your api documentation. Set to nil and mount - # it yourself if you need to. - config.mount_at = "/docs" - - # This defaults to Rails.root if left nil. If you're providing documentation for an engine using a dummy application - # it can be useful to set this to your engines root.. E.g. Application::Engine.root - config.root = nil - - # This is where rspec_api_documentation outputs the JSON files. This is configurable within RAD, and so is - # configurable here. - config.doc_path = "doc/api/v1" - - # The title of the documentation -- If your project has a name, you'll want to put it here. - config.title = "#{Rails.application.class.parent_name.titleize} V1" - - # The main layout view for all documentation pages. By default this is pretty basic, but you may want to use your own - # application layout. - config.layout = "apitome/application" - - # We're using highlight.js (https://github.com/isagalaev/highlight.js) for code highlighting, and it comes with some - # great themes. You can check http://softwaremaniacs.org/media/soft/highlight/test.html for themes, and enter the - # theme as lowercase/underscore. - config.code_theme = "default" - - # This allows you to override the css manually. You typically want to require `apitome/application` within the - # override, but if you want to override it entirely you can do so. - config.css_override = nil - - # This allows you to override the javascript manually. You typically want to require `apitome/application` within the - # override, but if you want to override it entirely you can do so. - config.js_override = nil - - # You can provide a "README" style markdown file for the documentation, which is a useful place to include general - # information. This path is relative to your doc_path configuration. - config.readme = "../../../README.md" - - # Apitome can render the documentation into a single page that uses scrollspy, or it can render the documentation on - # individual pages on demand. This allows you to specify which one you want, as a single page may impact performance. - config.single_page = true - - # Restrict access to documentation - # - if ENV["APITOME_USER"] && ENV["APITOME_PASSWORD"] - Apitome::DocsController.http_basic_authenticate_with( - name: ENV["APITOME_USER"], - password: ENV["APITOME_PASSWORD"], - only: [:index] - ) - end -end if defined? Apitome diff --git a/config/initializers/application_controller_renderer.rb b/config/initializers/application_controller_renderer.rb new file mode 100644 index 0000000..89d2efa --- /dev/null +++ b/config/initializers/application_controller_renderer.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# ActiveSupport::Reloader.to_prepare do +# ApplicationController.renderer.defaults.merge!( +# http_host: 'example.org', +# https: false +# ) +# end diff --git a/config/initializers/bullet.rb b/config/initializers/bullet.rb index c2410a5..da6d204 100644 --- a/config/initializers/bullet.rb +++ b/config/initializers/bullet.rb @@ -1,7 +1,9 @@ -Rails.application.config.after_initialize do - Bullet.enable = true - Bullet.bullet_logger = true - Bullet.console = true - Bullet.rails_logger = true - Bullet.add_footer = true -end if defined?(Bullet) +if defined?(Bullet) + Rails.application.config.after_initialize do + Bullet.enable = true + Bullet.bullet_logger = true + Bullet.console = true + Bullet.rails_logger = true + Bullet.add_footer = true + end +end diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb new file mode 100644 index 0000000..5a6a32d --- /dev/null +++ b/config/initializers/cookies_serializer.rb @@ -0,0 +1,5 @@ +# Be sure to restart your server when you modify this file. + +# Specify a serializer for the signed and encrypted cookie jars. +# Valid options are :json, :marshal, and :hybrid. +Rails.application.config.action_dispatch.cookies_serializer = :json diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb new file mode 100644 index 0000000..f7ffdd7 --- /dev/null +++ b/config/initializers/cors.rb @@ -0,0 +1,18 @@ +# Be sure to restart your server when you modify this file. + +# Avoid CORS issues when API is called from the frontend app. +# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests. + +# Read more: https://github.com/cyu/rack-cors + +if ENV["CORS_ORIGINS"].present? + Rails.application.config.middleware.insert_before 0, Rack::Cors do + allow do + origins(*ENV.fetch("CORS_ORIGINS").split(",")) + + resource "*", + headers: :any, + methods: %i[get post put patch delete options head] + end + end +end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb deleted file mode 100644 index 2f42a74..0000000 --- a/config/initializers/devise.rb +++ /dev/null @@ -1,266 +0,0 @@ -# Use this hook to configure devise mailer, warden hooks and so forth. -# Many of these configuration options can be set straight in your model. -Devise.setup do |config| - # The secret key used by Devise. Devise uses this key to generate - # random tokens. Changing this key will render invalid all existing - # confirmation, reset password and unlock tokens in the database. - # Devise will use the `secret_key_base` on Rails 4+ applications as its `secret_key` - # by default. You can change it below and use your own secret key. - - config.secret_key = ENV["SECRET_KEY_BASE"] - - # ==> Mailer Configuration - # Configure the e-mail address which will be shown in Devise::Mailer, - # note that it will be overwritten if you use your own mailer class - # with default "from" parameter. - config.mailer_sender = app_config.noreply - - # Configure the class responsible to send e-mails. - # config.mailer = 'Devise::Mailer' - - # ==> ORM configuration - # Load and configure the ORM. Supports :active_record (default) and - # :mongoid (bson_ext recommended) by default. Other ORMs may be - # available as additional gems. - require "devise/orm/active_record" - - # ==> Configuration for any authentication mechanism - # Configure which keys are used when authenticating a user. The default is - # just :email. You can configure it to use [:username, :subdomain], so for - # authenticating a user, both parameters are required. Remember that those - # parameters are used only when authenticating and not when retrieving from - # session. If you need permissions, you should implement that in a before filter. - # You can also supply a hash where the value is a boolean determining whether - # or not authentication should be aborted when the value is not present. - # config.authentication_keys = [:email] - - # Configure parameters from the request object used for authentication. Each entry - # given should be a request method and it will automatically be passed to the - # find_for_authentication method and considered in your model lookup. For instance, - # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. - # The same considerations mentioned for authentication_keys also apply to request_keys. - # config.request_keys = [] - - # Configure which authentication keys should be case-insensitive. - # These keys will be downcased upon creating or modifying a user and when used - # to authenticate or find a user. Default is :email. - config.case_insensitive_keys = [:email] - - # Configure which authentication keys should have whitespace stripped. - # These keys will have whitespace before and after removed upon creating or - # modifying a user and when used to authenticate or find a user. Default is :email. - config.strip_whitespace_keys = [:email] - - # Tell if authentication through request.params is enabled. True by default. - # It can be set to an array that will enable params authentication only for the - # given strategies, for example, `config.params_authenticatable = [:database]` will - # enable it only for database (email + password) authentication. - # config.params_authenticatable = true - - # Tell if authentication through HTTP Auth is enabled. False by default. - # It can be set to an array that will enable http authentication only for the - # given strategies, for example, `config.http_authenticatable = [:database]` will - # enable it only for database authentication. The supported strategies are: - # :database = Support basic authentication with authentication key + password - # config.http_authenticatable = false - - # If 401 status code should be returned for AJAX requests. True by default. - # config.http_authenticatable_on_xhr = true - - # The realm used in Http Basic Authentication. 'Application' by default. - # config.http_authentication_realm = 'Application' - - # It will change confirmation, password recovery and other workflows - # to behave the same regardless if the e-mail provided was right or wrong. - # Does not affect registerable. - # config.paranoid = true - - # By default Devise will store the user in session. You can skip storage for - # particular strategies by setting this option. - # Notice that if you are skipping storage for all authentication paths, you - # may want to disable generating routes to Devise's sessions controller by - # passing skip: :sessions to `devise_for` in your config/routes.rb - config.skip_session_storage = [:http_auth] - - # By default, Devise cleans up the CSRF token on authentication to - # avoid CSRF token fixation attacks. This means that, when using AJAX - # requests for sign in and sign up, you need to get a new CSRF token - # from the server. You can disable this option at your own risk. - # config.clean_up_csrf_token_on_authentication = true - - # ==> Configuration for :database_authenticatable - # For bcrypt, this is the cost for hashing the password and defaults to 10. If - # using other encryptors, it sets how many times you want the password re-encrypted. - # - # Limiting the stretches to just one in testing will increase the performance of - # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use - # a value less than 10 in other environments. Note that, for bcrypt (the default - # encryptor), the cost increases exponentially with the number of stretches (e.g. - # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). - config.stretches = Rails.env.test? ? 1 : 10 - - # Setup a pepper to generate the encrypted password. - # config.pepper = '' - - # ==> Configuration for :confirmable - # A period that the user is allowed to access the website even without - # confirming their account. For instance, if set to 2.days, the user will be - # able to access the website for two days without confirming their account, - # access will be blocked just in the third day. Default is 0.days, meaning - # the user cannot access the website without confirming their account. - # config.allow_unconfirmed_access_for = 2.days - - # A period that the user is allowed to confirm their account before their - # token becomes invalid. For example, if set to 3.days, the user can confirm - # their account within 3 days after the mail was sent, but on the fourth day - # their account can't be confirmed with the token any more. - # Default is nil, meaning there is no restriction on how long a user can take - # before confirming their account. - # config.confirm_within = 3.days - - # If true, requires any email changes to be confirmed (exactly the same way as - # initial account confirmation) to be applied. Requires additional unconfirmed_email - # db field (see migrations). Until confirmed, new email is stored in - # unconfirmed_email column, and copied to email column on successful confirmation. - config.reconfirmable = true - - # Defines which key will be used when confirming an account - # config.confirmation_keys = [:email] - - # ==> Configuration for :rememberable - # The time the user will be remembered without asking for credentials again. - # config.remember_for = 2.weeks - - # Invalidates all the remember me tokens when the user signs out. - config.expire_all_remember_me_on_sign_out = true - - # If true, extends the user's remember period when remembered via cookie. - # config.extend_remember_period = false - - # Options to be passed to the created cookie. For instance, you can set - # secure: true in order to force SSL only cookies. - # config.rememberable_options = {} - - # ==> Configuration for :validatable - # Range for password length. - config.password_length = 6..128 - - # Email regex used to validate email formats. It simply asserts that - # one (and only one) @ exists in the given string. This is mainly - # to give user feedback and not to assert the e-mail validity. - # config.email_regexp = /\A[^@]+@[^@]+\z/ - - # ==> Configuration for :timeoutable - # The time you want to timeout the user session without activity. After this - # time the user will be asked for credentials again. Default is 30 minutes. - # config.timeout_in = 30.minutes - - # If true, expires auth token on session timeout. - # config.expire_auth_token_on_timeout = false - - # ==> Configuration for :lockable - # Defines which strategy will be used to lock an account. - # :failed_attempts = Locks an account after a number of failed attempts to sign in. - # :none = No lock strategy. You should handle locking by yourself. - # config.lock_strategy = :failed_attempts - - # Defines which key will be used when locking and unlocking an account - # config.unlock_keys = [:email] - - # Defines which strategy will be used to unlock an account. - # :email = Sends an unlock link to the user email - # :time = Re-enables login after a certain amount of time (see :unlock_in below) - # :both = Enables both strategies - # :none = No unlock strategy. You should handle unlocking by yourself. - # config.unlock_strategy = :both - - # Number of authentication tries before locking an account if lock_strategy - # is failed attempts. - # config.maximum_attempts = 20 - - # Time interval to unlock the account if :time is enabled as unlock_strategy. - # config.unlock_in = 1.hour - - # Warn on the last attempt before the account is locked. - # config.last_attempt_warning = true - - # ==> Configuration for :recoverable - # - # Defines which key will be used when recovering the password for an account - # config.reset_password_keys = [:email] - - # Time interval you can reset your password with a reset password key. - # Don't put a too small interval or your users won't have the time to - # change their passwords. - config.reset_password_within = 6.hours - - # When set to false, does not sign a user in automatically after their password is - # reset. Defaults to true, so a user is signed in automatically after a reset. - # config.sign_in_after_reset_password = true - - # ==> Configuration for :encryptable - # Allow you to use another encryption algorithm besides bcrypt (default). You can use - # :sha1, :sha512 or encryptors from others authentication tools as :clearance_sha1, - # :authlogic_sha512 (then you should set stretches above to 20 for default behavior) - # and :restful_authentication_sha1 (then you should set stretches to 10, and copy - # REST_AUTH_SITE_KEY to pepper). - # - # Require the `devise-encryptable` gem when using anything other than bcrypt - # config.encryptor = :sha512 - - # ==> Scopes configuration - # Turn scoped views on. Before rendering "sessions/new", it will first check for - # "users/sessions/new". It's turned off by default because it's slower if you - # are using only default views. - # config.scoped_views = false - - # Configure the default scope given to Warden. By default it's the first - # devise role declared in your routes (usually :user). - # config.default_scope = :user - - # Set this configuration to false if you want /users/sign_out to sign out - # only the current scope. By default, Devise signs out all scopes. - # config.sign_out_all_scopes = true - - # ==> Navigation configuration - # Lists the formats that should be treated as navigational. Formats like - # :html, should redirect to the sign in page when the user does not have - # access, but formats like :xml or :json, should return 401. - # - # If you have any extra navigational formats, like :iphone or :mobile, you - # should add them to the navigational formats lists. - # - # The "*/*" below is required to match Internet Explorer requests. - # config.navigational_formats = ['*/*', :html] - - # The default HTTP method used to sign out a resource. Default is :delete. - config.sign_out_via = :delete - - # ==> OmniAuth - # Add a new OmniAuth provider. Check the wiki for more information on setting - # up on your models and hooks. - # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' - - # ==> Warden configuration - # If you want to use other strategies, that are not supported by Devise, or - # change the failure app, you can configure them inside the config.warden block. - # - # config.warden do |manager| - # manager.intercept_401 = false - # manager.default_strategies(scope: :user).unshift :some_external_strategy - # end - - # ==> Mountable engine configurations - # When using Devise inside an engine, let's call it `MyEngine`, and this engine - # is mountable, there are some extra configurations to be taken into account. - # The following options are available, assuming the engine is mounted as: - # - # mount MyEngine, at: '/my_engine' - # - # The router that invoked `devise_for`, in the example above, would be: - # config.router_name = :my_engine - # - # When using OmniAuth, Devise cannot automatically set OmniAuth path, - # so you need to do it manually. For the users scope, it would be: - # config.omniauth_path_prefix = '/my_engine/users/auth' -end diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb index 2494b78..4a994e1 100644 --- a/config/initializers/filter_parameter_logging.rb +++ b/config/initializers/filter_parameter_logging.rb @@ -1,4 +1,4 @@ # Be sure to restart your server when you modify this file. # Configure sensitive parameters which will be filtered from the log file. -Rails.application.config.filter_parameters += %i(password auth_token) +Rails.application.config.filter_parameters += [:password] diff --git a/config/initializers/generators.rb b/config/initializers/generators.rb index d62e9a9..ec08c65 100644 --- a/config/initializers/generators.rb +++ b/config/initializers/generators.rb @@ -1,4 +1,4 @@ -app_config.app_generators do |g| +Rails.application.config.app_generators do |g| g.fixture_replacement :factory_girl, dir: "spec/factories" g.stylesheets false g.javascripts false diff --git a/config/initializers/health_check.rb b/config/initializers/health_check.rb index 69c06b2..3f307d1 100644 --- a/config/initializers/health_check.rb +++ b/config/initializers/health_check.rb @@ -16,8 +16,8 @@ config.http_status_for_error_object = 500 # You can customize which checks happen on a standard health check - config.standard_checks = %w(database migrations site) + config.standard_checks = %w[database migrations site] # You can set what tests are run with the 'full' or 'all' parameter - config.full_checks = %w(database migrations site cache) + config.full_checks = %w[database migrations site cache] end diff --git a/config/initializers/mail_safe.rb b/config/initializers/mail_safe.rb deleted file mode 100644 index 8643c23..0000000 --- a/config/initializers/mail_safe.rb +++ /dev/null @@ -1,3 +0,0 @@ -# allow send emails to the @example.com -# -MailSafe::Config.internal_address_definition = /^.*@example\.com$/i if defined?(MailSafe::Config) diff --git a/config/initializers/mailer.rb b/config/initializers/mailer.rb deleted file mode 100644 index 767c357..0000000 --- a/config/initializers/mailer.rb +++ /dev/null @@ -1 +0,0 @@ -ActionMailer::Base.default_url_options[:host] = app_config.host diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index 72aca7e..dc18996 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -2,4 +2,3 @@ # Add new mime types for use in respond_to blocks: # Mime::Type.register "text/richtext", :rtf -# Mime::Type.register_alias "text/html", :iphone diff --git a/config/initializers/rack_cors.rb b/config/initializers/rack_cors.rb deleted file mode 100644 index 86219c0..0000000 --- a/config/initializers/rack_cors.rb +++ /dev/null @@ -1,14 +0,0 @@ -app_config.middleware.insert_before "Warden::Manager", "Rack::Cors" do - allow do - # Allow requests from domains: - # e.g. origins('api.example.com', 'next.example.com') - # - origins(*ENV.fetch("ALLOW_REQUESTS_FROM", app_config.host).split(",")) - - resource( - "*", - headers: :any, - methods: %i(get post put delete patch options) - ) - end -end diff --git a/config/initializers/requires.rb b/config/initializers/requires.rb deleted file mode 100644 index a7177d2..0000000 --- a/config/initializers/requires.rb +++ /dev/null @@ -1,5 +0,0 @@ -# Require recursively all files in the lib/extensions -Dir[Rails.root.join("lib/extensions/**/*.rb")].each { |f| require(f) } - -# Require only files placed in the lib folder -Dir[Rails.root.join("lib/*.rb")].each { |f| require(f) } diff --git a/config/initializers/rspec_api_documentation.rb b/config/initializers/rspec_api_documentation.rb deleted file mode 100644 index ab70c4c..0000000 --- a/config/initializers/rspec_api_documentation.rb +++ /dev/null @@ -1,9 +0,0 @@ -RspecApiDocumentation.configure do |config| - config.format = :json - config.docs_dir = Rails.root.join("doc", "api", "v1") - config.request_headers_to_include = ["Accept", "X-Auth-Token"] - config.response_headers_to_include = ["Content-Type"] - config.curl_host = "http://#{app_config.host}" - config.curl_headers_to_filter = ["Cookie", "Host", "Content-Type", "Origin"] - config.keep_source_order = true -end if defined? RspecApiDocumentation diff --git a/config/initializers/simple_token_authentication.rb b/config/initializers/simple_token_authentication.rb deleted file mode 100644 index 3388396..0000000 --- a/config/initializers/simple_token_authentication.rb +++ /dev/null @@ -1,64 +0,0 @@ -SimpleTokenAuthentication.configure do |config| - # Configure the session persistence policy after a successful sign in, - # in other words, if the authentication token acts as a signin token. - # If true, user is stored in the session and the authentication token and - # email may be provided only once. - # If false, users must provide their authentication token and email at every request. - # config.sign_in_token = false - - # Configure the name of the HTTP headers watched for authentication. - # - # Default header names for a given token authenticatable entity follow the pattern: - # { entity: { authentication_token: 'X-Entity-Token', email: 'X-Entity-Email'} } - # - # When several token authenticatable models are defined, custom header names - # can be specified for none, any, or all of them. - # - # Note: when using the identifiers options, this option behaviour is modified. - # Please see the example below. - # - # Examples - # - # Given User and SuperAdmin are token authenticatable, - # When the following configuration is used: - # `config.header_names = { super_admin: { authentication_token: 'X-Admin-Auth-Token' } }` - # Then the token authentification handler for User watches the following headers: - # `X-User-Token, X-User-Email` - # And the token authentification handler for SuperAdmin watches the following headers: - # `X-Admin-Auth-Token, X-SuperAdmin-Email` - # - # When the identifiers option is set: - # `config.identifiers = { super_admin: :phone_number }` - # Then both the header names identifier key and default value are modified accordingly: - # `config.header_names = { super_admin: { phone_number: 'X-SuperAdmin-PhoneNumber' } }` - # - # config.header_names = { user: { authentication_token: 'X-User-Token', email: 'X-User-Email' } } - - # Configure the name of the attribute used to identify the user for authentication. - # That attribute must exist in your model. - # - # The default identifiers follow the pattern: - # { entity: 'email' } - # - # Note: the identifer must match your Devise configuration, - # see https://github.com/plataformatec/devise/wiki/How-To:-Allow-users-to-sign-in-using-their-username-or-email-address#tell-devise-to-use-username-in-the-authentication_keys - # - # Note: setting this option does modify the header_names behaviour, - # see the header_names section above. - # - # Example: - # - # `config.identifiers = { super_admin: 'phone_number', user: 'uuid' }` - # - # config.identifiers = { user: 'email' } - - # Configure the Devise trackable strategy integration. - # - # If true, tracking is disabled for token authentication: signing in through - # token authentication won't modify the Devise trackable statistics. - # - # If false, given Devise trackable is configured for the relevant model, - # then signing in through token authentication will be tracked as any other sign in. - # - # config.skip_devise_trackable = true -end diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb index 7cafc93..bbfc396 100644 --- a/config/initializers/wrap_parameters.rb +++ b/config/initializers/wrap_parameters.rb @@ -5,8 +5,10 @@ # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. ActiveSupport.on_load(:action_controller) do - include ActionController::ParamsWrapper - - # Enable parameter wrapping for JSON - wrap_parameters(format: %i(json url_encoded_form)) + wrap_parameters format: [:json] end + +# To enable root element in JSON for ActiveRecord objects. +# ActiveSupport.on_load(:active_record) do +# self.include_root_in_json = true +# end diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml deleted file mode 100644 index 26a10f2..0000000 --- a/config/locales/devise.en.yml +++ /dev/null @@ -1,60 +0,0 @@ -# Additional translations at https://github.com/plataformatec/devise/wiki/I18n - -en: - devise: - confirmations: - confirmed: "Your email address has been successfully confirmed." - send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." - send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes." - failure: - already_authenticated: "You are already signed in." - inactive: "Your account is not activated yet." - invalid: "Invalid %{authentication_keys} or password." - locked: "Your account is locked." - last_attempt: "You have one more attempt before your account is locked." - not_found_in_database: "Invalid %{authentication_keys} or password." - timeout: "Your session expired. Please sign in again to continue." - unauthenticated: "You need to sign in or sign up before continuing." - unconfirmed: "You have to confirm your email address before continuing." - mailer: - confirmation_instructions: - subject: "Confirmation instructions" - reset_password_instructions: - subject: "Reset password instructions" - unlock_instructions: - subject: "Unlock instructions" - omniauth_callbacks: - failure: "Could not authenticate you from %{kind} because \"%{reason}\"." - success: "Successfully authenticated from %{kind} account." - passwords: - no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." - send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." - 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." - registrations: - destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." - signed_up: "Welcome! You have signed up successfully." - signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." - signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." - signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." - update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirm link to confirm your new email address." - updated: "Your account has been updated successfully." - sessions: - signed_in: "Signed in successfully." - signed_out: "Signed out successfully." - already_signed_out: "Signed out successfully." - unlocks: - send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." - send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." - unlocked: "Your account has been unlocked successfully. Please sign in to continue." - errors: - messages: - already_confirmed: "was already confirmed, please try signing in" - confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" - expired: "has expired, please request a new one" - not_found: "not found" - not_locked: "was not locked" - not_saved: - one: "1 error prohibited this %{resource} from being saved:" - other: "%{count} errors prohibited this %{resource} from being saved:" diff --git a/config/locales/en.yml b/config/locales/en.yml index d526dea..decc5a8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -16,15 +16,18 @@ # # 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 http://guides.rubyonrails.org/i18n.html. en: hello: "Hello world" - time: - formats: - short_date: "%x" - long_date: "%a, %b %d, %Y" - us: "%m/%d/%Y %I:%M %p" - us_date: "%m/%d/%Y" - us_time: "%I:%M %p" diff --git a/config/locales/errors.en.yml b/config/locales/errors.en.yml new file mode 100644 index 0000000..5ce9f63 --- /dev/null +++ b/config/locales/errors.en.yml @@ -0,0 +1,7 @@ +en: + errors: + invalid_credentials: Invalid credentials + unauthorized: Authorization required + record_not_found: Record not found + route_not_found: Route not found + custom_error: Custom error message diff --git a/config/puma.rb b/config/puma.rb new file mode 100644 index 0000000..7d3431e --- /dev/null +++ b/config/puma.rb @@ -0,0 +1,15 @@ +workers Integer(ENV["WEB_CONCURRENCY"] || 2) +threads_count = Integer(ENV["RAILS_MAX_THREADS"] || 5) +threads threads_count, threads_count + +preload_app! + +rackup DefaultRackup +port ENV["PORT"] || 3000 +environment ENV["RACK_ENV"] || "development" + +on_worker_boot do + # Worker specific setup for Rails 4.1+ + # See: https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server#on-worker-boot + ActiveRecord::Base.establish_connection +end diff --git a/config/routes.rb b/config/routes.rb index 2d0f9e4..2565c29 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,11 +1,8 @@ Rails.application.routes.draw do - scope defaults: { format: :json } do - devise_for :users, only: [] - end - - namespace :v1, defaults: { format: "json" } do - devise_scope :user do - post "users/sign_in", to: "sessions#create" - end + namespace :v1, defaults: { format: "jsonapi" } do + resources :registrations, only: :create + resources :tokens, only: :create + resource :profile, only: %i[show update destroy] + resources :users, only: %i[index show] end end diff --git a/config/rubocop.yml b/config/rubocop.yml deleted file mode 100644 index cc6bd9e..0000000 --- a/config/rubocop.yml +++ /dev/null @@ -1,58 +0,0 @@ -Rails: - Enabled: true - -AllCops: - DisplayCopNames: true - Exclude: - - bin/**/* - - db/**/* - - lib/templates/**/* - - vendor/**/* - -Style/Documentation: - Description: 'Document classes and non-namespace modules.' - Enabled: false - -Style/MethodCalledOnDoEndBlock: - Enabled: true - -Style/CollectionMethods: - Enabled: true - -Style/SymbolArray: - Description: 'Use %i or %I for arrays of symbols.' - Enabled: true - -Style/StringLiterals: - EnforcedStyle: double_quotes - -Metrics/LineLength: - Description: 'Limit lines to 120 characters.' - Max: 120 - -Layout/EndAlignment: - EnforcedStyleAlignWith: variable - -Layout/AlignParameters: - EnforcedStyle: with_fixed_indentation - SupportedStyles: - - with_first_parameter - - with_fixed_indentation - -Bundler/OrderedGems: - Enabled: false - -Lint/ScriptPermission: - Enabled: false - -Rails/FilePath: - Enabled: false - -Style/FrozenStringLiteralComment: - Enabled: false - -Style/MultilineIfModifier: - Enabled: false - -Style/PercentLiteralDelimiters: - Enabled: false diff --git a/config/secrets.yml b/config/secrets.yml index f98bc51..13c1ec8 100644 --- a/config/secrets.yml +++ b/config/secrets.yml @@ -1,15 +1,32 @@ -default: &default - secret_token: <%= ENV['SECRET_TOKEN'] %> - secret_key_base: <%= ENV['SECRET_KEY_BASE'] %> +# Be sure to restart your server when you modify this file. + +# Your secret key is used for verifying the integrity of signed cookies. +# If you change this key, all old signed cookies will become invalid! + +# Make sure the secret is at least 30 characters and all random, +# no regular words or you'll be exposed to dictionary attacks. +# You can use `rails secret` to generate a secure secret key. + +# Make sure the secrets in this file are kept private +# if you're sharing your code publicly. + +# Shared secrets are available across all environments. + +# shared: +# api_key: a1B2c3D4e5F6 + +# Environmental secrets are only available for that specific environment. development: - <<: *default + secret_key_base: fe10da2c2f8cac7ef34a508d284832dfec21a019f5fb01872bd8b5162f9f34dac43d28fda64be8fa0c0c5c1cdc01e51e7f588130de59ac13d53e1002c44983c9 test: - <<: *default + secret_key_base: 3565ea3c4f8cd650441c6137af6203803f6c7848105d3cbb84e469b9454e5a6635fa9327554f30ab27ab17c1d268df53035f7022a53f3858a6f6412b1331aa98 -staging: - <<: *default +# Do not keep production secrets in the unencrypted secrets file. +# Instead, either read values from the environment. +# Or, use `bin/rails secrets:setup` to configure encrypted secrets +# and move the `production:` environment over there. production: - <<: *default + secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> diff --git a/config/spring.rb b/config/spring.rb new file mode 100644 index 0000000..9fa7863 --- /dev/null +++ b/config/spring.rb @@ -0,0 +1,6 @@ +%w[ + .ruby-version + .rbenv-vars + tmp/restart.txt + tmp/caching-dev.txt +].each { |path| Spring.watch(path) } 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/20130319140714_create_users.rb b/db/migrate/20130319140714_create_users.rb new file mode 100644 index 0000000..010540a --- /dev/null +++ b/db/migrate/20130319140714_create_users.rb @@ -0,0 +1,12 @@ +class CreateUsers < ActiveRecord::Migration[5.2] + def change + create_table :users do |t| + t.string :full_name + t.string :email, null: false + t.string :password_digest, null: false + t.timestamps + end + + add_index :users, :email, unique: true + end +end diff --git a/db/migrate/20130319140714_devise_create_users.rb b/db/migrate/20130319140714_devise_create_users.rb deleted file mode 100644 index b91e775..0000000 --- a/db/migrate/20130319140714_devise_create_users.rb +++ /dev/null @@ -1,46 +0,0 @@ -class DeviseCreateUsers < ActiveRecord::Migration - def change - create_table(:users) do |t| - ## Database authenticatable - t.string :email, null: false, default: '' - t.string :encrypted_password, null: false, default: '' - - ## Recoverable - t.string :reset_password_token - t.datetime :reset_password_sent_at - - ## Rememberable - t.datetime :remember_created_at - - ## Trackable - t.integer :sign_in_count, default: 0 - t.datetime :current_sign_in_at - t.datetime :last_sign_in_at - t.string :current_sign_in_ip - t.string :last_sign_in_ip - - ## Confirmable - # t.string :confirmation_token - # t.datetime :confirmed_at - # t.datetime :confirmation_sent_at - # t.string :unconfirmed_email # Only if using reconfirmable - - ## Lockable - # t.integer :failed_attempts, default: 0 # Only if lock strategy is :failed_attempts - # t.string :unlock_token # Only if unlock strategy is :email or :both - # t.datetime :locked_at - - ## Token authenticatable - t.string :authentication_token - - - t.timestamps - end - - add_index :users, :email, unique: true - add_index :users, :reset_password_token, unique: true - # add_index :users, :confirmation_token, unique: true - # add_index :users, :unlock_token, unique: true - add_index :users, :authentication_token, unique: true - end -end diff --git a/db/schema.rb b/db/schema.rb index 02ac22c..d3eaba1 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1,4 +1,3 @@ -# encoding: UTF-8 # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. @@ -11,29 +10,18 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20130319140714) do +ActiveRecord::Schema.define(version: 2013_03_19_140714) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" - create_table "users", force: true do |t| - t.string "email", default: "", null: false - t.string "encrypted_password", default: "", null: false - t.string "reset_password_token" - t.datetime "reset_password_sent_at" - t.datetime "remember_created_at" - t.integer "sign_in_count", default: 0 - t.datetime "current_sign_in_at" - t.datetime "last_sign_in_at" - t.string "current_sign_in_ip" - t.string "last_sign_in_ip" - t.string "authentication_token" - t.datetime "created_at" - t.datetime "updated_at" + create_table "users", force: :cascade do |t| + t.string "full_name" + t.string "email", null: false + t.string "password_digest", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["email"], name: "index_users_on_email", unique: true end - add_index "users", ["authentication_token"], name: "index_users_on_authentication_token", unique: true, using: :btree - add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree - add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree - end diff --git a/db/seeds/development/all.seeds.rb b/db/seeds/development/all.seeds.rb index 6afebab..7f90429 100644 --- a/db/seeds/development/all.seeds.rb +++ b/db/seeds/development/all.seeds.rb @@ -1 +1 @@ -FactoryGirl.create :user +FactoryBot.create :user diff --git a/doc/api/v1/errors/request_to_unexisting_page.json b/doc/api/v1/errors/request_to_unexisting_page.json deleted file mode 100644 index 5a33232..0000000 --- a/doc/api/v1/errors/request_to_unexisting_page.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "resource": "Errors", - "http_method": "GET", - "route": "/not-found", - "description": "Request to unexisting page", - "explanation": null, - "parameters": [ - - ], - "response_fields": [ - - ], - "requests": [ - { - "request_method": "GET", - "request_path": "/not-found", - "request_body": null, - "request_headers": { - "Accept": "application/json" - }, - "request_query_parameters": { - }, - "request_content_type": null, - "response_status": 404, - "response_status_text": "Not Found", - "response_body": "{\"error\":{\"id\":\"f5576af0-303e-4ca4-b421-ba741116d4a4\",\"status\":404,\"error\":\"Not Found\",\"validations\":null}}", - "response_headers": { - "Content-Type": "application/json; charset=utf-8" - }, - "response_content_type": "application/json; charset=utf-8", - "curl": "curl \"http://localhost:5000/not-found\" -X GET \\\n\t-H \"Accept: application/json\"" - } - ] -} \ No newline at end of file diff --git a/doc/api/v1/index.json b/doc/api/v1/index.json deleted file mode 100644 index 015fd87..0000000 --- a/doc/api/v1/index.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "resources": [ - { - "name": "Errors", - "examples": [ - { - "description": "Request to unexisting page", - "link": "errors/request_to_unexisting_page.json", - "groups": "all" - } - ] - }, - { - "name": "Sessions", - "examples": [ - { - "description": "Sign in with valid password", - "link": "sessions/sign_in_with_valid_password.json", - "groups": "all" - }, - { - "description": "Sign in with invalid password", - "link": "sessions/sign_in_with_invalid_password.json", - "groups": "all" - } - ] - } - ] -} \ No newline at end of file diff --git a/doc/api/v1/index.md b/doc/api/v1/index.md new file mode 100644 index 0000000..3cae6dc --- /dev/null +++ b/doc/api/v1/index.md @@ -0,0 +1,25 @@ +# API Documentation + + +## Profiles + +* [Retrive Profile](profiles/retrive_profile.md) +* [Update Profile](profiles/update_profile.md) +* [Update Profile with empty password and invalid email](profiles/update_profile_with_empty_password_and_invalid_email.md) +* [Delete Profile](profiles/delete_profile.md) + +## Registration + +* [Create User](registration/create_user.md) + +## Tokens + +* [Create Token](tokens/create_token.md) +* [Create Token with invalid password](tokens/create_token_with_invalid_password.md) + +## Users + +* [List Users](users/list_users.md) +* [Retrive User](users/retrive_user.md) +* [Retrive User with invalid id](users/retrive_user_with_invalid_id.md) + diff --git a/doc/api/v1/profiles/delete_profile.md b/doc/api/v1/profiles/delete_profile.md new file mode 100644 index 0000000..eaa67da --- /dev/null +++ b/doc/api/v1/profiles/delete_profile.md @@ -0,0 +1,50 @@ +# Profiles API + +## Delete Profile + +### DELETE /v1/profile +### Request + +#### Headers + +
Content-Type: application/vnd.api+json +Accept: application/vnd.api+json +Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1MzY3NjExODgsInN1YiI6MTY2fQ.qgWchRvG9luv0IVp-sl-xfmFaIPtwTKcwttUozDUdcI+ +#### Route + +
DELETE /v1/profile+ +#### Body + +
{"data":{"type":"profile_requests","attributes":{"full_name":null,"email":null,"password":null}}}
+
+#### cURL
+
+curl "http://localhost:5000/v1/profile" -d '{"data":{"type":"profile_requests","attributes":{"full_name":null,"email":null,"password":null}}}' -X DELETE \
+ -H "Content-Type: application/vnd.api+json" \
+ -H "Accept: application/vnd.api+json" \
+ -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1MzY3NjExODgsInN1YiI6MTY2fQ.qgWchRvG9luv0IVp-sl-xfmFaIPtwTKcwttUozDUdcI"
+
+### Response
+
+#### Headers
+
+Content-Type: application/vnd.api+json; charset=utf-8+ +#### Status + +
200 OK+ +#### Body + +
{
+ "data": {
+ "id": "166",
+ "type": "users",
+ "attributes": {
+ "email": "user4@example.com",
+ "full_name": "Dr. Joana Heathcote"
+ }
+ }
+}
diff --git a/doc/api/v1/profiles/retrive_profile.md b/doc/api/v1/profiles/retrive_profile.md
new file mode 100644
index 0000000..c31b0c7
--- /dev/null
+++ b/doc/api/v1/profiles/retrive_profile.md
@@ -0,0 +1,50 @@
+# Profiles API
+
+## Retrive Profile
+
+### GET /v1/profile
+### Request
+
+#### Headers
+
+Content-Type: application/vnd.api+json +Accept: application/vnd.api+json +Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1MzY3NjExODcsInN1YiI6MTYzfQ.S1ejGuAOao-cIYvsSnbaETAG-1UCEtDZcDsdV-SeETs+ +#### Route + +
GET /v1/profile+ +#### Query Parameters + +
{"data":{"type":"profile_requests","attributes":{"full_name":null,"email":null,"password":null}}}:
+
+#### cURL
+
+curl -g "http://localhost:5000/v1/profile" -X GET \ + -H "Content-Type: application/vnd.api+json" \ + -H "Accept: application/vnd.api+json" \ + -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1MzY3NjExODcsInN1YiI6MTYzfQ.S1ejGuAOao-cIYvsSnbaETAG-1UCEtDZcDsdV-SeETs"+ +### Response + +#### Headers + +
Content-Type: application/vnd.api+json; charset=utf-8+ +#### Status + +
200 OK+ +#### Body + +
{
+ "data": {
+ "id": "163",
+ "type": "users",
+ "attributes": {
+ "email": "user1@example.com",
+ "full_name": "Joe Marks II"
+ }
+ }
+}
diff --git a/doc/api/v1/profiles/update_profile.md b/doc/api/v1/profiles/update_profile.md
new file mode 100644
index 0000000..9bc5291
--- /dev/null
+++ b/doc/api/v1/profiles/update_profile.md
@@ -0,0 +1,59 @@
+# Profiles API
+
+## Update Profile
+
+### PATCH /v1/profile
+
+### Parameters
+
+| Name | Description | Required | Scope |
+|------|-------------|----------|-------|
+| full_name | full name | false | |
+| email | email | false | |
+| password | password | false | |
+
+### Request
+
+#### Headers
+
+Content-Type: application/vnd.api+json +Accept: application/vnd.api+json +Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1MzY3NjExODcsInN1YiI6MTY0fQ.khNxDZk0-l7OVUIJEdr7JVmYE8oCmrH8WaorMAWk9Oc+ +#### Route + +
PATCH /v1/profile+ +#### Body + +
{"data":{"type":"profile_requests","attributes":{"full_name":"Example User Updated","email":"user_updated@example.com","password":"new_password"}}}
+
+#### cURL
+
+curl "http://localhost:5000/v1/profile" -d '{"data":{"type":"profile_requests","attributes":{"full_name":"Example User Updated","email":"user_updated@example.com","password":"new_password"}}}' -X PATCH \
+ -H "Content-Type: application/vnd.api+json" \
+ -H "Accept: application/vnd.api+json" \
+ -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1MzY3NjExODcsInN1YiI6MTY0fQ.khNxDZk0-l7OVUIJEdr7JVmYE8oCmrH8WaorMAWk9Oc"
+
+### Response
+
+#### Headers
+
+Content-Type: application/vnd.api+json; charset=utf-8+ +#### Status + +
200 OK+ +#### Body + +
{
+ "data": {
+ "id": "164",
+ "type": "users",
+ "attributes": {
+ "email": "user_updated@example.com",
+ "full_name": "Example User Updated"
+ }
+ }
+}
diff --git a/doc/api/v1/profiles/update_profile_with_empty_password_and_invalid_email.md b/doc/api/v1/profiles/update_profile_with_empty_password_and_invalid_email.md
new file mode 100644
index 0000000..1f68e7c
--- /dev/null
+++ b/doc/api/v1/profiles/update_profile_with_empty_password_and_invalid_email.md
@@ -0,0 +1,65 @@
+# Profiles API
+
+## Update Profile with empty password and invalid email
+
+### PATCH /v1/profile
+
+### Parameters
+
+| Name | Description | Required | Scope |
+|------|-------------|----------|-------|
+| full_name | full name | false | |
+| email | email | false | |
+| password | password | false | |
+
+### Request
+
+#### Headers
+
+Content-Type: application/vnd.api+json +Accept: application/vnd.api+json +Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1MzY3NjExODgsInN1YiI6MTY1fQ.AoMwSorPTETdEFEM1CUTKbEKEQzKhTGRW_yxh4KDPL0+ +#### Route + +
PATCH /v1/profile+ +#### Body + +
{"data":{"type":"profile_requests","attributes":{"full_name":"Example User Updated","email":"invalid","password":""}}}
+
+#### cURL
+
+curl "http://localhost:5000/v1/profile" -d '{"data":{"type":"profile_requests","attributes":{"full_name":"Example User Updated","email":"invalid","password":""}}}' -X PATCH \
+ -H "Content-Type: application/vnd.api+json" \
+ -H "Accept: application/vnd.api+json" \
+ -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1MzY3NjExODgsInN1YiI6MTY1fQ.AoMwSorPTETdEFEM1CUTKbEKEQzKhTGRW_yxh4KDPL0"
+
+### Response
+
+#### Headers
+
+Content-Type: application/vnd.api+json; charset=utf-8+ +#### Status + +
422 Unprocessable Entity+ +#### Body + +
{
+ "errors": [
+ {
+ "source": {
+ "pointer": "/data/attributes/password"
+ },
+ "detail": "is too short (minimum is 6 characters)"
+ },
+ {
+ "source": {
+ "pointer": "/data/attributes/email"
+ },
+ "detail": "is invalid"
+ }
+ ]
+}
diff --git a/doc/api/v1/registration/create_user.md b/doc/api/v1/registration/create_user.md
new file mode 100644
index 0000000..ebe4569
--- /dev/null
+++ b/doc/api/v1/registration/create_user.md
@@ -0,0 +1,57 @@
+# Registration API
+
+## Create User
+
+### POST /v1/registrations
+
+### Parameters
+
+| Name | Description | Required | Scope |
+|------|-------------|----------|-------|
+| full_name | full name | false | |
+| email | email | true | |
+| password | password | true | |
+
+### Request
+
+#### Headers
+
+Content-Type: application/vnd.api+json +Accept: application/vnd.api+json+ +#### Route + +
POST /v1/registrations+ +#### Body + +
{"data":{"type":"registration_requests","attributes":{"full_name":"Example User","email":"user@example.com","password":"123456"}}}
+
+#### cURL
+
+curl "http://localhost:5000/v1/registrations" -d '{"data":{"type":"registration_requests","attributes":{"full_name":"Example User","email":"user@example.com","password":"123456"}}}' -X POST \
+ -H "Content-Type: application/vnd.api+json" \
+ -H "Accept: application/vnd.api+json"
+
+### Response
+
+#### Headers
+
+Content-Type: application/vnd.api+json; charset=utf-8+ +#### Status + +
201 Created+ +#### Body + +
{
+ "data": {
+ "id": "167",
+ "type": "users",
+ "attributes": {
+ "email": "user@example.com",
+ "full_name": "Example User"
+ }
+ }
+}
diff --git a/doc/api/v1/sessions/sign_in_with_invalid_password.json b/doc/api/v1/sessions/sign_in_with_invalid_password.json
deleted file mode 100644
index f079d29..0000000
--- a/doc/api/v1/sessions/sign_in_with_invalid_password.json
+++ /dev/null
@@ -1,43 +0,0 @@
-{
- "resource": "Sessions",
- "http_method": "POST",
- "route": "/v1/users/sign_in",
- "description": "Sign in with invalid password",
- "explanation": null,
- "parameters": [
- {
- "required": true,
- "name": "email",
- "description": "Email"
- },
- {
- "required": true,
- "name": "password",
- "description": "Password"
- }
- ],
- "response_fields": [
-
- ],
- "requests": [
- {
- "request_method": "POST",
- "request_path": "/v1/users/sign_in",
- "request_body": "email=user2%40example.com&password=",
- "request_headers": {
- "Accept": "application/json"
- },
- "request_query_parameters": {
- },
- "request_content_type": "application/x-www-form-urlencoded",
- "response_status": 401,
- "response_status_text": "Unauthorized",
- "response_body": "{\"error\":{\"id\":\"19acd4f1-2fa1-4c6d-9d95-6cf8d7e8aaa7\",\"status\":401,\"error\":\"Invalid email or password.\",\"validations\":null}}",
- "response_headers": {
- "Content-Type": "application/json; charset=utf-8"
- },
- "response_content_type": "application/json; charset=utf-8",
- "curl": "curl \"http://localhost:5000/v1/users/sign_in\" -d 'email=user2%40example.com&password=' -X POST \\\n\t-H \"Accept: application/json\""
- }
- ]
-}
\ No newline at end of file
diff --git a/doc/api/v1/sessions/sign_in_with_valid_password.json b/doc/api/v1/sessions/sign_in_with_valid_password.json
deleted file mode 100644
index f1672a8..0000000
--- a/doc/api/v1/sessions/sign_in_with_valid_password.json
+++ /dev/null
@@ -1,43 +0,0 @@
-{
- "resource": "Sessions",
- "http_method": "POST",
- "route": "/v1/users/sign_in",
- "description": "Sign in with valid password",
- "explanation": null,
- "parameters": [
- {
- "required": true,
- "name": "email",
- "description": "Email"
- },
- {
- "required": true,
- "name": "password",
- "description": "Password"
- }
- ],
- "response_fields": [
-
- ],
- "requests": [
- {
- "request_method": "POST",
- "request_path": "/v1/users/sign_in",
- "request_body": "email=user1%40example.com&password=123456",
- "request_headers": {
- "Accept": "application/json"
- },
- "request_query_parameters": {
- },
- "request_content_type": "application/x-www-form-urlencoded",
- "response_status": 201,
- "response_status_text": "Created",
- "response_body": "{\"user\":{\"id\":1,\"authentication_token\":\"ac2zWyKFhnoaWEggD7tn\",\"email\":\"user1@example.com\"}}",
- "response_headers": {
- "Content-Type": "application/json; charset=utf-8"
- },
- "response_content_type": "application/json; charset=utf-8",
- "curl": "curl \"http://localhost:5000/v1/users/sign_in\" -d 'email=user1%40example.com&password=123456' -X POST \\\n\t-H \"Accept: application/json\""
- }
- ]
-}
\ No newline at end of file
diff --git a/doc/api/v1/tokens/create_token.md b/doc/api/v1/tokens/create_token.md
new file mode 100644
index 0000000..be71e87
--- /dev/null
+++ b/doc/api/v1/tokens/create_token.md
@@ -0,0 +1,55 @@
+# Tokens API
+
+## Create Token
+
+### POST /v1/tokens
+
+### Parameters
+
+| Name | Description | Required | Scope |
+|------|-------------|----------|-------|
+| email | email | true | |
+| password | password | true | |
+
+### Request
+
+#### Headers
+
+Content-Type: application/vnd.api+json +Accept: application/vnd.api+json+ +#### Route + +
POST /v1/tokens+ +#### Body + +
{"data":{"type":"token_requests","attributes":{"email":"user@example.com","password":"123456"}}}
+
+#### cURL
+
+curl "http://localhost:5000/v1/tokens" -d '{"data":{"type":"token_requests","attributes":{"email":"user@example.com","password":"123456"}}}' -X POST \
+ -H "Content-Type: application/vnd.api+json" \
+ -H "Accept: application/vnd.api+json"
+
+### Response
+
+#### Headers
+
+Content-Type: application/vnd.api+json; charset=utf-8+ +#### Status + +
201 Created+ +#### Body + +
{
+ "data": {
+ "id": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1MzY3NjExODgsInN1YiI6MTY4fQ.42J7f4PUabczP3pmUpQNcmtG4GFmXpHA17VaWmnQqK0",
+ "type": "jwt_tokens",
+ "attributes": {
+ "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1MzY3NjExODgsInN1YiI6MTY4fQ.42J7f4PUabczP3pmUpQNcmtG4GFmXpHA17VaWmnQqK0"
+ }
+ }
+}
diff --git a/doc/api/v1/tokens/create_token_with_invalid_password.md b/doc/api/v1/tokens/create_token_with_invalid_password.md
new file mode 100644
index 0000000..d65086d
--- /dev/null
+++ b/doc/api/v1/tokens/create_token_with_invalid_password.md
@@ -0,0 +1,54 @@
+# Tokens API
+
+## Create Token with invalid password
+
+### POST /v1/tokens
+
+### Parameters
+
+| Name | Description | Required | Scope |
+|------|-------------|----------|-------|
+| email | email | true | |
+| password | password | true | |
+
+### Request
+
+#### Headers
+
+Content-Type: application/vnd.api+json +Accept: application/vnd.api+json+ +#### Route + +
POST /v1/tokens+ +#### Body + +
{"data":{"type":"token_requests","attributes":{"email":"user@example.com","password":"invalid"}}}
+
+#### cURL
+
+curl "http://localhost:5000/v1/tokens" -d '{"data":{"type":"token_requests","attributes":{"email":"user@example.com","password":"invalid"}}}' -X POST \
+ -H "Content-Type: application/vnd.api+json" \
+ -H "Accept: application/vnd.api+json"
+
+### Response
+
+#### Headers
+
+Content-Type: application/json; charset=utf-8+ +#### Status + +
422 Unprocessable Entity+ +#### Body + +
{
+ "errors": [
+ {
+ "code": "invalid_credentials",
+ "detail": "Invalid credentials"
+ }
+ ]
+}
diff --git a/doc/api/v1/users/list_users.md b/doc/api/v1/users/list_users.md
new file mode 100644
index 0000000..856fb01
--- /dev/null
+++ b/doc/api/v1/users/list_users.md
@@ -0,0 +1,76 @@
+# Users API
+
+## List Users
+
+### GET /v1/users
+### Request
+
+#### Headers
+
+Content-Type: application/vnd.api+json +Accept: application/vnd.api+json +Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1MzY3NjExODgsInN1YiI6MTczfQ.Abxi19K-az8G3cuhq_ti18PBbtKAc_f3RUJuHVlXc1Q+ +#### Route + +
GET /v1/users+ +#### Query Parameters + +
{"data":{"type":"user_requests","attributes":{"full_name":null,"email":null,"password":null}}}:
+
+#### cURL
+
+curl -g "http://localhost:5000/v1/users" -X GET \ + -H "Content-Type: application/vnd.api+json" \ + -H "Accept: application/vnd.api+json" \ + -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1MzY3NjExODgsInN1YiI6MTczfQ.Abxi19K-az8G3cuhq_ti18PBbtKAc_f3RUJuHVlXc1Q"+ +### Response + +#### Headers + +
Content-Type: application/vnd.api+json; charset=utf-8+ +#### Status + +
200 OK+ +#### Body + +
{
+ "data": [
+ {
+ "id": "170",
+ "type": "users",
+ "attributes": {
+ "email": "user5@example.com",
+ "full_name": "Danial Gibson"
+ }
+ },
+ {
+ "id": "171",
+ "type": "users",
+ "attributes": {
+ "email": "user6@example.com",
+ "full_name": "Yukiko Ledner"
+ }
+ },
+ {
+ "id": "172",
+ "type": "users",
+ "attributes": {
+ "email": "user7@example.com",
+ "full_name": "Elois Kiehn"
+ }
+ },
+ {
+ "id": "173",
+ "type": "users",
+ "attributes": {
+ "email": "user8@example.com",
+ "full_name": "Mr. Kelly Connelly"
+ }
+ }
+ ]
+}
diff --git a/doc/api/v1/users/retrive_user.md b/doc/api/v1/users/retrive_user.md
new file mode 100644
index 0000000..3148649
--- /dev/null
+++ b/doc/api/v1/users/retrive_user.md
@@ -0,0 +1,57 @@
+# Users API
+
+## Retrive User
+
+### GET /v1/users/:id
+
+### Parameters
+
+| Name | Description | Required | Scope |
+|------|-------------|----------|-------|
+| id | user id | true | |
+
+### Request
+
+#### Headers
+
+Content-Type: application/vnd.api+json +Accept: application/vnd.api+json +Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1MzY3NjExODgsInN1YiI6MTc1fQ.DSxpLhyzDiN4nlSW2ikMxIZyIcBUA4au93HT18Ka9WA+ +#### Route + +
GET /v1/users/174+ +#### Query Parameters + +
{"data":{"type":"user_requests","attributes":{"full_name":null,"email":null,"password":null}}}:
+
+#### cURL
+
+curl -g "http://localhost:5000/v1/users/174" -X GET \ + -H "Content-Type: application/vnd.api+json" \ + -H "Accept: application/vnd.api+json" \ + -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1MzY3NjExODgsInN1YiI6MTc1fQ.DSxpLhyzDiN4nlSW2ikMxIZyIcBUA4au93HT18Ka9WA"+ +### Response + +#### Headers + +
Content-Type: application/vnd.api+json; charset=utf-8+ +#### Status + +
200 OK+ +#### Body + +
{
+ "data": {
+ "id": "174",
+ "type": "users",
+ "attributes": {
+ "email": "user9@example.com",
+ "full_name": "Bryon Gutkowski"
+ }
+ }
+}
diff --git a/doc/api/v1/users/retrive_user_with_invalid_id.md b/doc/api/v1/users/retrive_user_with_invalid_id.md
new file mode 100644
index 0000000..ab13045
--- /dev/null
+++ b/doc/api/v1/users/retrive_user_with_invalid_id.md
@@ -0,0 +1,55 @@
+# Users API
+
+## Retrive User with invalid id
+
+### GET /v1/users/:id
+
+### Parameters
+
+| Name | Description | Required | Scope |
+|------|-------------|----------|-------|
+| id | user id | true | |
+
+### Request
+
+#### Headers
+
+Content-Type: application/vnd.api+json +Accept: application/vnd.api+json +Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1MzY3NjExODgsInN1YiI6MTc2fQ.SM3fZ97H8I_cvj-xHR18FmfdvXYK_XMtObYmAs4k1lQ+ +#### Route + +
GET /v1/users/0+ +#### Query Parameters + +
{"data":{"type":"user_requests","attributes":{"full_name":null,"email":null,"password":null}}}:
+
+#### cURL
+
+curl -g "http://localhost:5000/v1/users/0" -X GET \ + -H "Content-Type: application/vnd.api+json" \ + -H "Accept: application/vnd.api+json" \ + -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1MzY3NjExODgsInN1YiI6MTc2fQ.SM3fZ97H8I_cvj-xHR18FmfdvXYK_XMtObYmAs4k1lQ"+ +### Response + +#### Headers + +
Content-Type: application/json; charset=utf-8+ +#### Status + +
404 Not Found+ +#### Body + +
{
+ "errors": [
+ {
+ "code": "record_not_found",
+ "detail": "Record not found"
+ }
+ ]
+}
diff --git a/spec/acceptance/errors_spec.rb b/spec/acceptance/errors_spec.rb
deleted file mode 100644
index 592dfa5..0000000
--- a/spec/acceptance/errors_spec.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-require "rails_helper"
-require "rspec_api_documentation/dsl"
-
-resource "Errors" do
- header "Accept", "application/json"
- subject(:response) { json_response_body }
-
- get "/not-found" do
- example_request "Request to unexisting page" do
- expect(response_status).to eq 404
- expect(response).to be_an_error_representation(:not_found, "Not Found")
- end
- end
-end
diff --git a/spec/acceptance/v1/sessions_spec.rb b/spec/acceptance/v1/sessions_spec.rb
deleted file mode 100644
index 1692f80..0000000
--- a/spec/acceptance/v1/sessions_spec.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-require "rails_helper"
-require "rspec_api_documentation/dsl"
-
-resource "Sessions" do
- header "Accept", "application/json"
-
- subject(:response) { json_response_body }
-
- post "/v1/users/sign_in" do
- let(:user) { create :user, password: "123456" }
-
- parameter :email, "Email", required: true
- parameter :password, "Password", required: true
-
- let(:email) { user.email }
-
- example_request "Sign in with valid password", password: "123456" do
- expect(response["user"]).to be_a_session_representation
- end
-
- example_request "Sign in with invalid password", password: "" do
- expect(response_status).to eq 401
- expect(response).to be_an_error_representation(:unauthorized, "Invalid email or password.")
- end
- end
-end
diff --git a/spec/api/v1/profiles_spec.rb b/spec/api/v1/profiles_spec.rb
new file mode 100644
index 0000000..91f6719
--- /dev/null
+++ b/spec/api/v1/profiles_spec.rb
@@ -0,0 +1,67 @@
+require "rails_helper"
+
+class ProfileRequest < ActiveModelSerializers::Model
+ attributes :id, :full_name, :email, :password
+end
+
+class ProfileRequestSerializer < ApplicationSerializer
+ attributes :full_name, :email, :password
+end
+
+resource "Profiles" do
+ include_context "with JSON API Headers"
+ include_context "with JSON API Authorization header"
+ include_context "with JSON API post body from request class"
+
+ get "/v1/profile" do
+ let(:request_class) { "profile_request" }
+
+ example "Retrive Profile" do
+ do_request
+
+ expect(response_status).to eq(200)
+ expect(response_body).to match_response_schema("v1/users")
+ end
+ end
+
+ patch "/v1/profile" do
+ parameter :full_name, "full name"
+ parameter :email, "email"
+ parameter :password, "password"
+
+ let(:full_name) { "Example User Updated" }
+ let(:email) { "user_updated@example.com" }
+ let(:password) { "new_password" }
+ let(:request_class) { "profile_request" }
+
+ example "Update Profile" do
+ do_request
+
+ expect(response_status).to eq(200)
+ expect(response_body).to match_response_schema("v1/user")
+ end
+
+ context "with invalid data" do
+ let(:password) { "" }
+ let(:email) { "invalid" }
+
+ example "Update Profile with empty password and invalid email" do
+ do_request
+
+ expect(response_status).to eq(422)
+ expect(response_body).to match_response_schema("v1/errors")
+ end
+ end
+ end
+
+ delete "/v1/profile" do
+ let(:request_class) { "profile_request" }
+
+ example "Delete Profile" do
+ do_request
+
+ expect(response_status).to eq(200)
+ expect(response_body).to match_response_schema("v1/user")
+ end
+ end
+end
diff --git a/spec/api/v1/registrations_spec.rb b/spec/api/v1/registrations_spec.rb
new file mode 100644
index 0000000..d4239cf
--- /dev/null
+++ b/spec/api/v1/registrations_spec.rb
@@ -0,0 +1,32 @@
+require "rails_helper"
+
+class RegistrationRequest < ActiveModelSerializers::Model
+ attributes :id, :full_name, :email, :password
+end
+
+class RegistrationRequestSerializer < ApplicationSerializer
+ attributes :full_name, :email, :password
+end
+
+resource "Registration" do
+ include_context "with JSON API Headers"
+ include_context "with JSON API post body from request class"
+
+ post "/v1/registrations" do
+ parameter :full_name, "full name"
+ parameter :email, "email", required: true
+ parameter :password, "password", required: true
+
+ let(:full_name) { "Example User" }
+ let(:email) { "user@example.com" }
+ let(:password) { "123456" }
+ let(:request_class) { "registration_request" }
+
+ example "Create User" do
+ do_request
+
+ expect(response_status).to eq(201)
+ expect(response_body).to match_response_schema("v1/user")
+ end
+ end
+end
diff --git a/spec/api/v1/tokens_spec.rb b/spec/api/v1/tokens_spec.rb
new file mode 100644
index 0000000..4c04439
--- /dev/null
+++ b/spec/api/v1/tokens_spec.rb
@@ -0,0 +1,46 @@
+require "rails_helper"
+
+class TokenRequest < ActiveModelSerializers::Model
+ attributes :id, :email, :password
+end
+
+class TokenRequestSerializer < ApplicationSerializer
+ attributes :email, :password
+end
+
+resource "Tokens" do
+ include_context "with JSON API Headers"
+
+ post "/v1/tokens" do
+ parameter :email, "email", required: true
+ parameter :password, "password", required: true
+
+ let(:email) { "user@example.com" }
+ let(:password) { "123456" }
+ let(:request_class) { "token_request" }
+
+ include_context "with JSON API post body from request class"
+
+ before do
+ create :user, email: email, password: "123456"
+ end
+
+ example "Create Token" do
+ do_request
+
+ expect(response_status).to eq(201)
+ expect(response_body).to match_response_schema("v1/jwt_token")
+ end
+
+ context "with invalid password" do
+ let(:password) { "invalid" }
+
+ example "Create Token with invalid password" do
+ do_request
+
+ expect(response_status).to eq(422)
+ expect(response_body).to match_response_schema("v1/errors")
+ end
+ end
+ end
+end
diff --git a/spec/api/v1/users_spec.rb b/spec/api/v1/users_spec.rb
new file mode 100644
index 0000000..ed8c5f7
--- /dev/null
+++ b/spec/api/v1/users_spec.rb
@@ -0,0 +1,56 @@
+require "rails_helper"
+
+class UserRequest < ActiveModelSerializers::Model
+ attributes :id, :full_name, :email, :password
+end
+
+class UserRequestSerializer < ApplicationSerializer
+ attributes :full_name, :email, :password
+end
+
+resource "Users" do
+ include_context "with JSON API Headers"
+ include_context "with JSON API Authorization header"
+ include_context "with JSON API post body from request class"
+
+ get "/v1/users" do
+ let(:request_class) { "user_request" }
+
+ before do
+ create_list :user, 3
+ end
+
+ example "List Users" do
+ do_request
+
+ expect(response_status).to eq(200)
+ expect(response_body).to match_response_schema("v1/users")
+ end
+ end
+
+ get "/v1/users/:id" do
+ parameter :id, "user id", required: true
+
+ let(:user) { create :user }
+ let(:id) { user.id }
+ let(:request_class) { "user_request" }
+
+ example "Retrive User" do
+ do_request
+
+ expect(response_status).to eq(200)
+ expect(response_body).to match_response_schema("v1/user")
+ end
+
+ context "with invalid id" do
+ let(:id) { 0 }
+
+ example "Retrive User with invalid id" do
+ do_request
+
+ expect(response_status).to eq(404)
+ expect(response_body).to match_response_schema("v1/errors")
+ end
+ end
+ end
+end
diff --git a/spec/factories/jwt_tokens.rb b/spec/factories/jwt_tokens.rb
new file mode 100644
index 0000000..06f3349
--- /dev/null
+++ b/spec/factories/jwt_tokens.rb
@@ -0,0 +1,13 @@
+FactoryBot.define do
+ factory :jwt_token do
+ transient do
+ subject { create :user }
+
+ id { subject.id }
+ end
+
+ payload { { "sub" => id } }
+
+ initialize_with { new(payload: payload) }
+ end
+end
diff --git a/spec/factories/sequences.rb b/spec/factories/sequences.rb
index 029f4ec..0b6c758 100644
--- a/spec/factories/sequences.rb
+++ b/spec/factories/sequences.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
sequence(:email) { |n| "user#{n}@example.com" }
sequence(:password) { "123456" }
end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index 10a95cd..6da016c 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -1,5 +1,6 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :user do
+ full_name { Faker::Name.name }
email
password
end
diff --git a/spec/interactors/create_jwt_spec.rb b/spec/interactors/create_jwt_spec.rb
new file mode 100644
index 0000000..7a7e125
--- /dev/null
+++ b/spec/interactors/create_jwt_spec.rb
@@ -0,0 +1,25 @@
+require "rails_helper"
+
+describe CreateJwt do
+ let(:interactor) { described_class.new(user_attributes) }
+ let(:context) { interactor.context }
+
+ let(:user_attributes) { attributes_for(:user).slice(:email, :password) }
+
+ context "when user does not exist" do
+ it_behaves_like "failure interactor"
+ end
+
+ context "when user exists" do
+ before do
+ create(:user, user_attributes)
+ end
+
+ it_behaves_like "success interactor"
+
+ it "sets token in context" do
+ interactor.run
+ expect(context.jwt_token).to be_present
+ end
+ end
+end
diff --git a/spec/models/error_spec.rb b/spec/models/error_spec.rb
new file mode 100644
index 0000000..00b15c0
--- /dev/null
+++ b/spec/models/error_spec.rb
@@ -0,0 +1,14 @@
+require "rails_helper"
+
+describe Error do
+ subject(:error) { described_class.new(code: :custom_error) }
+
+ it "serializable resource" do
+ expect { ActiveModelSerializers::SerializableResource.new(error).to_json }
+ .not_to raise_error
+ end
+
+ its(:code) { is_expected.to be(:custom_error) }
+ its(:status) { is_expected.to be(:internal_server_error) }
+ its(:detail) { is_expected.to eql("Custom error message") }
+end
diff --git a/spec/models/jwt_token_spec.rb b/spec/models/jwt_token_spec.rb
new file mode 100644
index 0000000..b4e7d41
--- /dev/null
+++ b/spec/models/jwt_token_spec.rb
@@ -0,0 +1,10 @@
+require "rails_helper"
+
+describe JwtToken do
+ subject(:jwt_token) { described_class.new(payload: { "sub" => "1" }) }
+
+ it "serializable resource" do
+ expect { ActiveModelSerializers::SerializableResource.new(jwt_token).to_json }
+ .not_to raise_error
+ end
+end
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index c7544c8..53ba992 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -1,55 +1,13 @@
-ENV["RAILS_ENV"] = "test"
+ENV["RAILS_ENV"] ||= "test"
+
require "spec_helper"
require File.expand_path("../config/environment", __dir__)
require "rspec/rails"
require "shoulda/matchers"
-Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }
+Dir[Rails.root.join("spec", "support", "**", "*.rb")].each { |f| require f }
RSpec.configure do |config|
- # If you're not using ActiveRecord, or you'd prefer not to run each of your
- # examples within a transaction, remove the following line or assign false
- # instead of true.
- config.use_transactional_fixtures = true
-
- # RSpec Rails can automatically mix in different behaviours to your tests
- # based on their file location, for example enabling you to call `get` and
- # `post` in specs under `spec/controllers`.
- #
- # You can disable this behaviour by removing the line below, and instead
- # explicitly tag your specs with their type, e.g.:
- #
- # RSpec.describe UsersController, :type => :controller do
- # # ...
- # end
- #
- # The different available types are documented in the features, such as in
- # https://relishapp.com/rspec/rspec-rails/docs
+ config.use_transactional_fixtures = false
config.infer_spec_type_from_file_location!
-
- config.include Rails.application.routes.url_helpers
- config.include EmailSpec::Helpers
- config.include EmailSpec::Matchers
- config.include Devise::TestHelpers, type: :controller
- config.include FactoryGirl::Syntax::Methods
- config.include Helpers
- config.include JsonSpec::Helpers
- config.include RailsApiFormat::Matchers
-
- config.before :suite do
- DatabaseCleaner.strategy = :transaction
- DatabaseCleaner.clean_with :truncation
- end
-
- config.before do
- ActionMailer::Base.deliveries.clear
- end
-
- config.before(:each) do
- DatabaseCleaner.start
- end
-
- config.after(:each) do
- DatabaseCleaner.clean
- end
end
diff --git a/spec/schemas/v1/definitions.json b/spec/schemas/v1/definitions.json
new file mode 100644
index 0000000..3c0b85c
--- /dev/null
+++ b/spec/schemas/v1/definitions.json
@@ -0,0 +1,30 @@
+{
+ "id": "file:/definitions.json#",
+
+ "definitions": {
+ "user": {
+ "required": ["attributes"],
+ "properties": {
+ "attributes": {
+ "required": ["email", "full_name"],
+ "properties": {
+ "email": { "type": "string" },
+ "full_name": { "type": ["string", "null"] }
+ }
+ }
+ }
+ },
+
+ "jwt_token": {
+ "required": ["attributes"],
+ "properties": {
+ "attributes": {
+ "required": ["token"],
+ "properties": {
+ "token": { "type": "string" }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/spec/schemas/v1/errors.json b/spec/schemas/v1/errors.json
new file mode 100644
index 0000000..2f127dd
--- /dev/null
+++ b/spec/schemas/v1/errors.json
@@ -0,0 +1,5 @@
+{
+ "allOf": [
+ { "$ref": "file:/jsonapi.json#/definitions/failure" }
+ ]
+}
diff --git a/spec/schemas/v1/jsonapi.json b/spec/schemas/v1/jsonapi.json
new file mode 100644
index 0000000..a79d271
--- /dev/null
+++ b/spec/schemas/v1/jsonapi.json
@@ -0,0 +1,381 @@
+{
+ "id": "file:/jsonapi.json#",
+
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "title": "JSON API Schema",
+ "description": "This is a schema for responses in the JSON API format. For more, see http://jsonapi.org",
+ "oneOf": [
+ {
+ "$ref": "#/definitions/success"
+ },
+ {
+ "$ref": "#/definitions/failure"
+ },
+ {
+ "$ref": "#/definitions/info"
+ }
+ ],
+
+ "definitions": {
+ "success": {
+ "type": "object",
+ "required": [
+ "data"
+ ],
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/data"
+ },
+ "included": {
+ "description": "To reduce the number of HTTP requests, servers **MAY** allow responses that include related resources along with the requested primary resources. Such responses are called \"compound documents\".",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/resource"
+ },
+ "uniqueItems": true
+ },
+ "meta": {
+ "$ref": "#/definitions/meta"
+ },
+ "links": {
+ "description": "Link members related to the primary data.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/links"
+ },
+ {
+ "$ref": "#/definitions/pagination"
+ }
+ ]
+ },
+ "jsonapi": {
+ "$ref": "#/definitions/jsonapi"
+ }
+ },
+ "additionalProperties": false
+ },
+ "failure": {
+ "type": "object",
+ "required": [
+ "errors"
+ ],
+ "properties": {
+ "errors": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/error"
+ },
+ "uniqueItems": true
+ },
+ "meta": {
+ "$ref": "#/definitions/meta"
+ },
+ "jsonapi": {
+ "$ref": "#/definitions/jsonapi"
+ },
+ "links": {
+ "$ref": "#/definitions/links"
+ }
+ },
+ "additionalProperties": false
+ },
+ "info": {
+ "type": "object",
+ "required": [
+ "meta"
+ ],
+ "properties": {
+ "meta": {
+ "$ref": "#/definitions/meta"
+ },
+ "links": {
+ "$ref": "#/definitions/links"
+ },
+ "jsonapi": {
+ "$ref": "#/definitions/jsonapi"
+ }
+ },
+ "additionalProperties": false
+ },
+
+ "meta": {
+ "description": "Non-standard meta-information that can not be represented as an attribute or relationship.",
+ "type": "object",
+ "additionalProperties": true
+ },
+ "data": {
+ "description": "The document's \"primary data\" is a representation of the resource or collection of resources targeted by a request.",
+ "oneOf": [
+ {
+ "$ref": "#/definitions/resource"
+ },
+ {
+ "description": "An array of resource objects, an array of resource identifier objects, or an empty array ([]), for requests that target resource collections.",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/resource"
+ },
+ "uniqueItems": true
+ },
+ {
+ "description": "null if the request is one that might correspond to a single resource, but doesn't currently.",
+ "type": "null"
+ }
+ ]
+ },
+ "resource": {
+ "description": "\"Resource objects\" appear in a JSON API document to represent resources.",
+ "type": "object",
+ "required": [
+ "type",
+ "id"
+ ],
+ "properties": {
+ "type": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "attributes": {
+ "$ref": "#/definitions/attributes"
+ },
+ "relationships": {
+ "$ref": "#/definitions/relationships"
+ },
+ "links": {
+ "$ref": "#/definitions/links"
+ },
+ "meta": {
+ "$ref": "#/definitions/meta"
+ }
+ },
+ "additionalProperties": false
+ },
+
+ "links": {
+ "description": "A resource object **MAY** contain references to other resource objects (\"relationships\"). Relationships may be to-one or to-many. Relationships can be specified by including a member in a resource's links object.",
+ "type": "object",
+ "properties": {
+ "self": {
+ "description": "A `self` member, whose value is a URL for the relationship itself (a \"relationship URL\"). This URL allows the client to directly manipulate the relationship. For example, it would allow a client to remove an `author` from an `article` without deleting the people resource itself.",
+ "type": "string",
+ "format": "uri-reference"
+ },
+ "related": {
+ "$ref": "#/definitions/link"
+ }
+ },
+ "additionalProperties": true
+ },
+ "link": {
+ "description": "A link **MUST** be represented as either: a string containing the link's URL or a link object.",
+ "oneOf": [
+ {
+ "description": "A string containing the link's URL.",
+ "type": "string",
+ "format": "uri-reference"
+ },
+ {
+ "type": "object",
+ "required": [
+ "href"
+ ],
+ "properties": {
+ "href": {
+ "description": "A string containing the link's URL.",
+ "type": "string",
+ "format": "uri-reference"
+ },
+ "meta": {
+ "$ref": "#/definitions/meta"
+ }
+ }
+ }
+ ]
+ },
+
+ "attributes": {
+ "description": "Members of the attributes object (\"attributes\") represent information about the resource object in which it's defined.",
+ "type": "object",
+ "patternProperties": {
+ "^(?!relationships$|links$|id$|type$)\\w[-\\w_]*$": {
+ "description": "Attributes may contain any valid JSON value."
+ }
+ },
+ "additionalProperties": false
+ },
+
+ "relationships": {
+ "description": "Members of the relationships object (\"relationships\") represent references from the resource object in which it's defined to other resource objects.",
+ "type": "object",
+ "patternProperties": {
+ "^(?!id$|type$)\\w[-\\w_]*$": {
+ "properties": {
+ "links": {
+ "$ref": "#/definitions/links"
+ },
+ "data": {
+ "description": "Member, whose value represents \"resource linkage\".",
+ "oneOf": [
+ {
+ "$ref": "#/definitions/relationshipToOne"
+ },
+ {
+ "$ref": "#/definitions/relationshipToMany"
+ }
+ ]
+ },
+ "meta": {
+ "$ref": "#/definitions/meta"
+ }
+ },
+ "anyOf": [
+ {"required": ["data"]},
+ {"required": ["meta"]},
+ {"required": ["links"]}
+ ],
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ },
+ "relationshipToOne": {
+ "description": "References to other resource objects in a to-one (\"relationship\"). Relationships can be specified by including a member in a resource's links object.",
+ "anyOf": [
+ {
+ "$ref": "#/definitions/empty"
+ },
+ {
+ "$ref": "#/definitions/linkage"
+ }
+ ]
+ },
+ "relationshipToMany": {
+ "description": "An array of objects each containing \"type\" and \"id\" members for to-many relationships.",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/linkage"
+ },
+ "uniqueItems": true
+ },
+ "empty": {
+ "description": "Describes an empty to-one relationship.",
+ "type": "null"
+ },
+ "linkage": {
+ "description": "The \"type\" and \"id\" to non-empty members.",
+ "type": "object",
+ "required": [
+ "type",
+ "id"
+ ],
+ "properties": {
+ "type": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "meta": {
+ "$ref": "#/definitions/meta"
+ }
+ },
+ "additionalProperties": false
+ },
+ "pagination": {
+ "type": "object",
+ "properties": {
+ "first": {
+ "description": "The first page of data",
+ "oneOf": [
+ { "type": "string", "format": "uri-reference" },
+ { "type": "null" }
+ ]
+ },
+ "last": {
+ "description": "The last page of data",
+ "oneOf": [
+ { "type": "string", "format": "uri-reference" },
+ { "type": "null" }
+ ]
+ },
+ "prev": {
+ "description": "The previous page of data",
+ "oneOf": [
+ { "type": "string", "format": "uri-reference" },
+ { "type": "null" }
+ ]
+ },
+ "next": {
+ "description": "The next page of data",
+ "oneOf": [
+ { "type": "string", "format": "uri-reference" },
+ { "type": "null" }
+ ]
+ }
+ }
+ },
+
+ "jsonapi": {
+ "description": "An object describing the server's implementation",
+ "type": "object",
+ "properties": {
+ "version": {
+ "type": "string"
+ },
+ "meta": {
+ "$ref": "#/definitions/meta"
+ }
+ },
+ "additionalProperties": false
+ },
+
+ "error": {
+ "type": "object",
+ "required": ["id", "status", "code", "title"],
+ "properties": {
+ "id": {
+ "description": "A unique identifier for this particular occurrence of the problem.",
+ "type": "string"
+ },
+ "links": {
+ "$ref": "#/definitions/links"
+ },
+ "status": {
+ "description": "The HTTP status code applicable to this problem, expressed as a string value.",
+ "type": "string"
+ },
+ "code": {
+ "description": "An application-specific error code, expressed as a string value.",
+ "type": "string"
+ },
+ "title": {
+ "description": "A short, human-readable summary of the problem. It **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization.",
+ "type": "string"
+ },
+ "detail": {
+ "description": "A human-readable explanation specific to this occurrence of the problem.",
+ "type": "string"
+ },
+ "source": {
+ "type": "object",
+ "properties": {
+ "pointer": {
+ "description": "A JSON Pointer [RFC6901] to the associated entity in the request document [e.g. \"/data\" for a primary data object, or \"/data/attributes/title\" for a specific attribute].",
+ "type": "string"
+ },
+ "parameter": {
+ "description": "A string indicating which query parameter caused the error.",
+ "type": "string"
+ }
+ }
+ },
+ "meta": {
+ "$ref": "#/definitions/meta"
+ }
+ },
+ "additionalProperties": false
+ }
+ }
+}
diff --git a/spec/schemas/v1/jwt_token.json b/spec/schemas/v1/jwt_token.json
new file mode 100644
index 0000000..812e119
--- /dev/null
+++ b/spec/schemas/v1/jwt_token.json
@@ -0,0 +1,10 @@
+{
+ "allOf": [
+ { "$ref": "file:/jsonapi.json#/definitions/success" },
+ {
+ "properties": {
+ "data": { "$ref": "file:/definitions.json#/definitions/jwt_token" }
+ }
+ }
+ ]
+}
diff --git a/spec/schemas/v1/user.json b/spec/schemas/v1/user.json
new file mode 100644
index 0000000..b1b19fe
--- /dev/null
+++ b/spec/schemas/v1/user.json
@@ -0,0 +1,10 @@
+{
+ "allOf": [
+ { "$ref": "file:/jsonapi.json#/definitions/success" },
+ {
+ "properties": {
+ "data": { "$ref": "file:/definitions.json#/definitions/user" }
+ }
+ }
+ ]
+}
diff --git a/spec/schemas/v1/users.json b/spec/schemas/v1/users.json
new file mode 100644
index 0000000..21fe9e8
--- /dev/null
+++ b/spec/schemas/v1/users.json
@@ -0,0 +1,12 @@
+{
+ "allOf": [
+ { "$ref": "file:/jsonapi.json#/definitions/success" },
+ {
+ "properties": {
+ "data": {
+ "items": { "$ref": "file:/definitions.json#/definitions/user" }
+ }
+ }
+ }
+ ]
+}
diff --git a/spec/serializers/paginated_array_serializer_spec.rb b/spec/serializers/paginated_array_serializer_spec.rb
deleted file mode 100644
index 2e8771a..0000000
--- a/spec/serializers/paginated_array_serializer_spec.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-require "rails_helper"
-
-describe PaginatedArraySerializer do
- let(:users) { Kaminari.paginate_array(build_list(:user, 3)).page(1) }
- let(:json) { ActiveModel::SerializableResource.serialize(users, serializer: PaginatedArraySerializer).to_json }
- let(:parsed_json) { parse_json(json) }
-
- it "returns json with meta" do
- expect(parsed_json).to include("meta")
- end
-
- it "returns meta with pagination info" do
- expect(parsed_json["meta"]).to include("pagination")
-
- expect(parsed_json["meta"]["pagination"]["total"]).to eq 3
- expect(parsed_json["meta"]["pagination"]["per_page"]).to eq 25
- expect(parsed_json["meta"]["pagination"]["page"]).to eq 1
- end
-end
diff --git a/spec/serializers/session_serializer_spec.rb b/spec/serializers/session_serializer_spec.rb
deleted file mode 100644
index 5fae7b9..0000000
--- a/spec/serializers/session_serializer_spec.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-require "rails_helper"
-
-describe SessionSerializer do
- let(:user) { build(:user) }
- let(:json) { ActiveModel::SerializableResource.serialize(user, serializer: described_class).to_json }
- let(:user_json) { parse_json(json)["user"] }
-
- it "returns user with session data" do
- expect(user_json).to be_a_session_representation
- end
-end
diff --git a/spec/serializers/user_serializer_spec.rb b/spec/serializers/user_serializer_spec.rb
deleted file mode 100644
index 6a2342b..0000000
--- a/spec/serializers/user_serializer_spec.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-require "rails_helper"
-
-describe UserSerializer do
- let(:user) { build :user, id: 1, authentication_token: "token" }
- let(:json) { ActiveModel::SerializableResource.serialize(user).to_json }
- let(:user_json) { parse_json(json)["user"] }
-
- it "returns user" do
- expect(user_json).to be_a_user_representation(user)
- end
-end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index eecfb1d..9c3435d 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -1,19 +1,13 @@
-# This file was generated by the `rails generate rspec:install` command. Conventionally, all
-# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
-# The generated `.rspec` file contains `--require spec_helper` which will cause this
-# file to always be loaded, without a need to explicitly require it in any files.
-#
-# Given that it is always loaded, you are encouraged to keep this file as
-# light-weight as possible. Requiring heavyweight dependencies from this file
-# will add to the boot time of your test suite on EVERY test run, even for an
-# individual file that may not need all of that loaded. Instead, make a
-# separate helper file that requires this one and then use it only in the specs
-# that actually need it.
-#
-# The `.rspec` file also contains a few flags that are not defaults but that
-# users commonly want.
-#
-# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
RSpec.configure do |config|
- config.order = :random
+ config.backtrace_exclusion_patterns << /\.bundle/
+
+ config.expect_with :rspec do |c|
+ c.syntax = :expect
+ end
+
+ config.mock_with :rspec do |mocks|
+ mocks.syntax = :expect
+ mocks.verify_partial_doubles = true
+ mocks.verify_doubled_constant_names = true
+ end
end
diff --git a/spec/support/database_cleaner.rb b/spec/support/database_cleaner.rb
new file mode 100644
index 0000000..1c75a4e
--- /dev/null
+++ b/spec/support/database_cleaner.rb
@@ -0,0 +1,23 @@
+require "database_cleaner"
+
+RSpec.configure do |config|
+ config.before(:suite) do
+ DatabaseCleaner.clean_with(:deletion)
+ end
+
+ config.before do
+ DatabaseCleaner.strategy = :transaction
+ end
+
+ config.before(:each, js: true) do
+ DatabaseCleaner.strategy = :deletion
+ end
+
+ config.before do
+ DatabaseCleaner.start
+ end
+
+ config.after do
+ DatabaseCleaner.clean
+ end
+end
diff --git a/spec/support/email.rb b/spec/support/email.rb
new file mode 100644
index 0000000..9dc8762
--- /dev/null
+++ b/spec/support/email.rb
@@ -0,0 +1,10 @@
+require "email_spec"
+
+RSpec.configure do |config|
+ config.include EmailSpec::Helpers
+ config.include EmailSpec::Matchers
+
+ config.before do
+ ActionMailer::Base.deliveries.clear
+ end
+end
diff --git a/spec/support/extensions/active_record/sliced.rb b/spec/support/extensions/active_record/sliced.rb
deleted file mode 100644
index d78678b..0000000
--- a/spec/support/extensions/active_record/sliced.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-ActiveRecord::Base.class_eval do
- def sliced_attributes(keys)
- attributes.slice(*keys)
- end
-end
diff --git a/spec/support/factory_bot.rb b/spec/support/factory_bot.rb
new file mode 100644
index 0000000..c7890e4
--- /dev/null
+++ b/spec/support/factory_bot.rb
@@ -0,0 +1,3 @@
+RSpec.configure do |config|
+ config.include FactoryBot::Syntax::Methods
+end
diff --git a/spec/support/helpers.rb b/spec/support/helpers.rb
deleted file mode 100644
index 0d34a7d..0000000
--- a/spec/support/helpers.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-module Helpers
- def setup_devise_mapping(mapping_name = :user)
- @request.env["devise.mapping"] = Devise.mappings[mapping_name]
- end
-
- def json_response_body
- parse_json(response_body)
- end
-end
diff --git a/spec/support/json_matchers.rb b/spec/support/json_matchers.rb
new file mode 100644
index 0000000..d80a0e4
--- /dev/null
+++ b/spec/support/json_matchers.rb
@@ -0,0 +1,3 @@
+require "json_matchers/rspec"
+
+JsonMatchers.schema_root = "spec/schemas"
diff --git a/spec/support/matchers/be_a_meta_representation_of.rb b/spec/support/matchers/be_a_meta_representation_of.rb
deleted file mode 100644
index 43f4c95..0000000
--- a/spec/support/matchers/be_a_meta_representation_of.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-RSpec::Matchers.define :be_a_meta_representation_of do |posts, params|
- match do |json|
- params = params.stringify_keys
-
- expect(json).to be
- expect(json).to include("pagination")
-
- expect(json["pagination"]["page"]).to eq params["page"]
- expect(json["pagination"]["per_page"]).to eq params["per_page"]
- expect(json["pagination"]["total"]).to eq posts.size
- end
-end
diff --git a/spec/support/matchers/be_a_session_representation.rb b/spec/support/matchers/be_a_session_representation.rb
deleted file mode 100644
index e874869..0000000
--- a/spec/support/matchers/be_a_session_representation.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-RSpec::Matchers.define :be_a_session_representation do
- match do |json|
- response_attributes = %w(
- id
- authentication_token
- email
- )
-
- expect(json).to be
- expect(json.keys).to match_array(response_attributes)
- end
-end
diff --git a/spec/support/matchers/be_a_user_representation.rb b/spec/support/matchers/be_a_user_representation.rb
deleted file mode 100644
index 1f3c29f..0000000
--- a/spec/support/matchers/be_a_user_representation.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-RSpec::Matchers.define :be_a_user_representation do |user|
- match do |json|
- response_attributes = user.sliced_attributes %w(
- id
- email
- )
-
- expect(json).to be
- expect(json).to include_attributes(response_attributes)
- end
-end
diff --git a/spec/support/matchers/include_attributes.rb b/spec/support/matchers/include_attributes.rb
deleted file mode 100644
index a6e7a70..0000000
--- a/spec/support/matchers/include_attributes.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-RSpec::Matchers.define :include_attributes do |expected|
- match do |actual|
- expect(actual).to RSpec::Matchers::BuiltIn::Include.new(expected.as_json)
- end
-end
diff --git a/spec/support/rspec_api_documentation.rb b/spec/support/rspec_api_documentation.rb
new file mode 100644
index 0000000..3707899
--- /dev/null
+++ b/spec/support/rspec_api_documentation.rb
@@ -0,0 +1,14 @@
+require "rspec_api_documentation"
+require "rspec_api_documentation/dsl"
+
+# rubocop:disable Style/WordArray
+RspecApiDocumentation.configure do |config|
+ config.format = :markdown
+ config.docs_dir = Rails.root.join("doc", "api", "v1")
+ config.request_headers_to_include = ["Accept", "Content-Type", "Authorization"]
+ config.response_headers_to_include = ["Content-Type"]
+ config.curl_host = "http://#{ENV.fetch('HOST')}"
+ config.curl_headers_to_filter = ["Cookie", "Host", "Origin"]
+ config.keep_source_order = true
+end
+# rubocop:enable Style/WordArray
diff --git a/spec/support/shared_contexts/json_api_headers.rb b/spec/support/shared_contexts/json_api_headers.rb
new file mode 100644
index 0000000..6f1b70e
--- /dev/null
+++ b/spec/support/shared_contexts/json_api_headers.rb
@@ -0,0 +1,17 @@
+shared_context "with JSON API Headers" do
+ header "Content-Type", "application/vnd.api+json"
+ header "Accept", "application/vnd.api+json"
+end
+
+shared_context "with JSON API Authorization header" do
+ let(:current_user) { create(:user) }
+ let(:jwt_token) { build(:jwt_token, subject: current_user) }
+ let(:authorization) { "Bearer #{jwt_token.token}" }
+
+ header "Authorization", :authorization
+end
+
+shared_context "with JSON API post body from request class" do
+ let(:request_resource) { request_class.classify.constantize.new(params) }
+ let(:raw_post) { ActiveModelSerializers::SerializableResource.new(request_resource).to_json }
+end
diff --git a/spec/support/shared_examples/api_endpoint_with_authorization.rb b/spec/support/shared_examples/api_endpoint_with_authorization.rb
new file mode 100644
index 0000000..4c6d59b
--- /dev/null
+++ b/spec/support/shared_examples/api_endpoint_with_authorization.rb
@@ -0,0 +1,12 @@
+# rubocop:disable RSpec/EmptyExampleGroup
+shared_examples "API endpoint with authorization" do
+ context "without authorization headers", document: false do
+ header "Authorization", ""
+
+ example_request "Request without authorization header" do
+ expect(response_status).to eq(401)
+ expect(response_body).to match_response_schema("error")
+ end
+ end
+end
+# rubocop:enable RSpec/EmptyExampleGroup
diff --git a/spec/support/shared_examples/failure_interactor.rb b/spec/support/shared_examples/failure_interactor.rb
new file mode 100644
index 0000000..dd5fb27
--- /dev/null
+++ b/spec/support/shared_examples/failure_interactor.rb
@@ -0,0 +1,7 @@
+shared_examples "failure interactor" do |params|
+ it "fails" do
+ interactor.run
+ expect(context).to be_failure
+ expect(context.message).to eql(params[:message]) if params
+ end
+end
diff --git a/spec/support/shared_examples/success_interactor.rb b/spec/support/shared_examples/success_interactor.rb
new file mode 100644
index 0000000..e4cf2b4
--- /dev/null
+++ b/spec/support/shared_examples/success_interactor.rb
@@ -0,0 +1,6 @@
+shared_examples "success interactor" do
+ it "success" do
+ interactor.run
+ expect(context).to be_success
+ end
+end