├── .gitignore ├── .ruby-version ├── .tool-versions ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── app ├── controllers │ ├── application_controller.rb │ ├── concerns │ │ └── .keep │ ├── todo_lists_controller.rb │ ├── todos_controller.rb │ └── users_controller.rb ├── jobs │ └── application_job.rb ├── mailers │ ├── application_mailer.rb │ └── user_mailer.rb └── models │ ├── application_record.rb │ ├── concerns │ └── .keep │ ├── todo.rb │ ├── todo_list.rb │ └── user.rb ├── bin ├── bundle ├── rails ├── rake ├── setup └── spring ├── config.ru ├── config ├── application.rb ├── boot.rb ├── credentials.yml.enc ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── application_controller_renderer.rb │ ├── backtrace_silencers.rb │ ├── cors.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ ├── mime_types.rb │ └── wrap_parameters.rb ├── locales │ └── en.yml ├── puma.rb ├── routes.rb └── spring.rb ├── db ├── migrate │ ├── 20191203023223_create_users.rb │ ├── 20191203035409_create_todos.rb │ ├── 20230208115402_create_todo_lists.rb │ └── 20230208115941_add_todo_list_id_to_todos.rb ├── schema.rb └── seeds.rb ├── lib └── tasks │ ├── .keep │ └── rubycritic.rake ├── log └── .keep ├── public └── robots.txt ├── test ├── controllers │ ├── .keep │ ├── todo_lists_controller │ │ ├── create_test.rb │ │ ├── destroy_test.rb │ │ ├── index_test.rb │ │ ├── show_test.rb │ │ └── update_test.rb │ ├── todos_controller │ │ ├── complete_test.rb │ │ ├── create_test.rb │ │ ├── destroy_test.rb │ │ ├── incomplete_test.rb │ │ ├── index_test.rb │ │ ├── show_test.rb │ │ └── update_test.rb │ └── users_controller │ │ ├── create_test.rb │ │ ├── destroy_test.rb │ │ └── show_test.rb ├── fixtures │ ├── .keep │ ├── files │ │ └── .keep │ ├── todo_lists.yml │ ├── todos.yml │ └── users.yml ├── integration │ └── .keep ├── mailers │ └── user_mailer_test.rb ├── models │ ├── .keep │ ├── todo_list_test.rb │ ├── todo_test.rb │ └── user_test.rb ├── support │ ├── authentication_header.rb │ ├── hash_schema_assertions.rb │ ├── regexp_patterns.rb │ ├── todo_assertions.rb │ └── todo_list_assertions.rb └── test_helper.rb ├── tmp └── .keep └── vendor └── .keep /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore the default SQLite database. 11 | /db/*.sqlite3 12 | /db/*.sqlite3-journal 13 | /db/*.sqlite3-* 14 | 15 | # Ignore all logfiles and tempfiles. 16 | /log/* 17 | /tmp/* 18 | !/log/.keep 19 | !/tmp/.keep 20 | 21 | # Ignore uploaded files in development. 22 | /storage/* 23 | !/storage/.keep 24 | .byebug_history 25 | 26 | # Ignore master key for decrypting credentials and more. 27 | /config/master.key 28 | 29 | /coverage 30 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-3.2.0 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | ruby 3.2.0 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 3 | 4 | ruby '3.2.0' 5 | 6 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' 7 | gem 'rails', '~> 7.0', '>= 7.0.4.2' 8 | 9 | # Use sqlite3 as the database for Active Record 10 | gem 'sqlite3', '~> 1.6' 11 | 12 | # Use Puma as the app server 13 | gem 'puma', '~> 6.0', '>= 6.0.2' 14 | 15 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 16 | # gem 'jbuilder', '~> 2.11', '>= 2.11.5' 17 | 18 | # Use Redis adapter to run Action Cable in production 19 | # gem 'redis', '~> 5.0', '>= 5.0.6' 20 | 21 | # Use Active Model has_secure_password 22 | # gem 'bcrypt', '~> 3.1', '>= 3.1.18' 23 | 24 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 25 | gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] 26 | 27 | # Reduces boot times through caching; required in config/boot.rb 28 | gem 'bootsnap', '~> 1.16', require: false 29 | 30 | # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] 31 | # gem 'image_processing', '~> 1.2' 32 | 33 | # Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible 34 | # gem 'rack-cors' 35 | 36 | group :development, :test do 37 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console 38 | gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] 39 | 40 | gem 'pry-byebug', '~> 3.10', '>= 3.10.1' 41 | 42 | gem 'letter_opener', '~> 1.8', '>= 1.8.1' 43 | 44 | gem 'did_you_mean', '~> 1.6', '>= 1.6.3' 45 | end 46 | 47 | group :development do 48 | gem 'rexml', '~> 3.2', '>= 3.2.5' 49 | 50 | gem 'rubycritic', '~> 4.7', require: false 51 | 52 | # Speed up commands on slow machines / big apps [https://github.com/rails/spring] 53 | # gem "spring" 54 | end 55 | 56 | group :test do 57 | gem 'simplecov', '~> 0.22.0', require: false 58 | end 59 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actioncable (7.0.4.2) 5 | actionpack (= 7.0.4.2) 6 | activesupport (= 7.0.4.2) 7 | nio4r (~> 2.0) 8 | websocket-driver (>= 0.6.1) 9 | actionmailbox (7.0.4.2) 10 | actionpack (= 7.0.4.2) 11 | activejob (= 7.0.4.2) 12 | activerecord (= 7.0.4.2) 13 | activestorage (= 7.0.4.2) 14 | activesupport (= 7.0.4.2) 15 | mail (>= 2.7.1) 16 | net-imap 17 | net-pop 18 | net-smtp 19 | actionmailer (7.0.4.2) 20 | actionpack (= 7.0.4.2) 21 | actionview (= 7.0.4.2) 22 | activejob (= 7.0.4.2) 23 | activesupport (= 7.0.4.2) 24 | mail (~> 2.5, >= 2.5.4) 25 | net-imap 26 | net-pop 27 | net-smtp 28 | rails-dom-testing (~> 2.0) 29 | actionpack (7.0.4.2) 30 | actionview (= 7.0.4.2) 31 | activesupport (= 7.0.4.2) 32 | rack (~> 2.0, >= 2.2.0) 33 | rack-test (>= 0.6.3) 34 | rails-dom-testing (~> 2.0) 35 | rails-html-sanitizer (~> 1.0, >= 1.2.0) 36 | actiontext (7.0.4.2) 37 | actionpack (= 7.0.4.2) 38 | activerecord (= 7.0.4.2) 39 | activestorage (= 7.0.4.2) 40 | activesupport (= 7.0.4.2) 41 | globalid (>= 0.6.0) 42 | nokogiri (>= 1.8.5) 43 | actionview (7.0.4.2) 44 | activesupport (= 7.0.4.2) 45 | builder (~> 3.1) 46 | erubi (~> 1.4) 47 | rails-dom-testing (~> 2.0) 48 | rails-html-sanitizer (~> 1.1, >= 1.2.0) 49 | activejob (7.0.4.2) 50 | activesupport (= 7.0.4.2) 51 | globalid (>= 0.3.6) 52 | activemodel (7.0.4.2) 53 | activesupport (= 7.0.4.2) 54 | activerecord (7.0.4.2) 55 | activemodel (= 7.0.4.2) 56 | activesupport (= 7.0.4.2) 57 | activestorage (7.0.4.2) 58 | actionpack (= 7.0.4.2) 59 | activejob (= 7.0.4.2) 60 | activerecord (= 7.0.4.2) 61 | activesupport (= 7.0.4.2) 62 | marcel (~> 1.0) 63 | mini_mime (>= 1.1.0) 64 | activesupport (7.0.4.2) 65 | concurrent-ruby (~> 1.0, >= 1.0.2) 66 | i18n (>= 1.6, < 2) 67 | minitest (>= 5.1) 68 | tzinfo (~> 2.0) 69 | addressable (2.8.1) 70 | public_suffix (>= 2.0.2, < 6.0) 71 | ast (2.4.2) 72 | axiom-types (0.1.1) 73 | descendants_tracker (~> 0.0.4) 74 | ice_nine (~> 0.11.0) 75 | thread_safe (~> 0.3, >= 0.3.1) 76 | bootsnap (1.16.0) 77 | msgpack (~> 1.2) 78 | builder (3.2.4) 79 | byebug (11.1.3) 80 | coderay (1.1.3) 81 | coercible (1.0.0) 82 | descendants_tracker (~> 0.0.1) 83 | concurrent-ruby (1.2.0) 84 | crass (1.0.6) 85 | date (3.3.3) 86 | descendants_tracker (0.0.4) 87 | thread_safe (~> 0.3, >= 0.3.1) 88 | did_you_mean (1.6.3) 89 | docile (1.4.0) 90 | equalizer (0.0.11) 91 | erubi (1.11.0) 92 | flay (2.13.0) 93 | erubi (~> 1.10) 94 | path_expander (~> 1.0) 95 | ruby_parser (~> 3.0) 96 | sexp_processor (~> 4.0) 97 | flog (4.6.6) 98 | path_expander (~> 1.0) 99 | ruby_parser (~> 3.1, > 3.1.0) 100 | sexp_processor (~> 4.8) 101 | globalid (1.1.0) 102 | activesupport (>= 5.0) 103 | i18n (1.12.0) 104 | concurrent-ruby (~> 1.0) 105 | ice_nine (0.11.2) 106 | kwalify (0.7.2) 107 | launchy (2.5.0) 108 | addressable (~> 2.7) 109 | letter_opener (1.8.1) 110 | launchy (>= 2.2, < 3) 111 | loofah (2.19.1) 112 | crass (~> 1.0.2) 113 | nokogiri (>= 1.5.9) 114 | mail (2.8.1) 115 | mini_mime (>= 0.1.1) 116 | net-imap 117 | net-pop 118 | net-smtp 119 | marcel (1.0.2) 120 | method_source (1.0.0) 121 | mini_mime (1.1.2) 122 | minitest (5.17.0) 123 | msgpack (1.6.0) 124 | net-imap (0.3.4) 125 | date 126 | net-protocol 127 | net-pop (0.1.2) 128 | net-protocol 129 | net-protocol (0.2.1) 130 | timeout 131 | net-smtp (0.3.3) 132 | net-protocol 133 | nio4r (2.5.8) 134 | nokogiri (1.14.1-arm64-darwin) 135 | racc (~> 1.4) 136 | nokogiri (1.14.1-x86_64-linux) 137 | racc (~> 1.4) 138 | parser (3.1.3.0) 139 | ast (~> 2.4.1) 140 | path_expander (1.1.1) 141 | pry (0.14.1) 142 | coderay (~> 1.1) 143 | method_source (~> 1.0) 144 | pry-byebug (3.10.1) 145 | byebug (~> 11.0) 146 | pry (>= 0.13, < 0.15) 147 | public_suffix (5.0.1) 148 | puma (6.0.2) 149 | nio4r (~> 2.0) 150 | racc (1.6.2) 151 | rack (2.2.6.2) 152 | rack-test (2.0.2) 153 | rack (>= 1.3) 154 | rails (7.0.4.2) 155 | actioncable (= 7.0.4.2) 156 | actionmailbox (= 7.0.4.2) 157 | actionmailer (= 7.0.4.2) 158 | actionpack (= 7.0.4.2) 159 | actiontext (= 7.0.4.2) 160 | actionview (= 7.0.4.2) 161 | activejob (= 7.0.4.2) 162 | activemodel (= 7.0.4.2) 163 | activerecord (= 7.0.4.2) 164 | activestorage (= 7.0.4.2) 165 | activesupport (= 7.0.4.2) 166 | bundler (>= 1.15.0) 167 | railties (= 7.0.4.2) 168 | rails-dom-testing (2.0.3) 169 | activesupport (>= 4.2.0) 170 | nokogiri (>= 1.6) 171 | rails-html-sanitizer (1.5.0) 172 | loofah (~> 2.19, >= 2.19.1) 173 | railties (7.0.4.2) 174 | actionpack (= 7.0.4.2) 175 | activesupport (= 7.0.4.2) 176 | method_source 177 | rake (>= 12.2) 178 | thor (~> 1.0) 179 | zeitwerk (~> 2.5) 180 | rainbow (3.1.1) 181 | rake (13.0.6) 182 | reek (6.1.2) 183 | kwalify (~> 0.7.0) 184 | parser (~> 3.1.0) 185 | rainbow (>= 2.0, < 4.0) 186 | rexml (3.2.5) 187 | ruby_parser (3.19.2) 188 | sexp_processor (~> 4.16) 189 | rubycritic (4.7.0) 190 | flay (~> 2.8) 191 | flog (~> 4.4) 192 | launchy (>= 2.0.0) 193 | parser (>= 2.6.0) 194 | rainbow (~> 3.0) 195 | reek (~> 6.0, < 7.0) 196 | ruby_parser (~> 3.8) 197 | simplecov (>= 0.17.0) 198 | tty-which (~> 0.4.0) 199 | virtus (~> 1.0) 200 | sexp_processor (4.16.1) 201 | simplecov (0.22.0) 202 | docile (~> 1.1) 203 | simplecov-html (~> 0.11) 204 | simplecov_json_formatter (~> 0.1) 205 | simplecov-html (0.12.3) 206 | simplecov_json_formatter (0.1.4) 207 | sqlite3 (1.6.0-arm64-darwin) 208 | sqlite3 (1.6.0-x86_64-linux) 209 | thor (1.2.1) 210 | thread_safe (0.3.6) 211 | timeout (0.3.1) 212 | tty-which (0.4.2) 213 | tzinfo (2.0.6) 214 | concurrent-ruby (~> 1.0) 215 | virtus (1.0.5) 216 | axiom-types (~> 0.1) 217 | coercible (~> 1.0) 218 | descendants_tracker (~> 0.0, >= 0.0.3) 219 | equalizer (~> 0.0, >= 0.0.9) 220 | websocket-driver (0.7.5) 221 | websocket-extensions (>= 0.1.0) 222 | websocket-extensions (0.1.5) 223 | zeitwerk (2.6.6) 224 | 225 | PLATFORMS 226 | arm64-darwin-21 227 | arm64-darwin-22 228 | x86_64-linux 229 | 230 | DEPENDENCIES 231 | bootsnap (~> 1.16) 232 | byebug 233 | did_you_mean (~> 1.6, >= 1.6.3) 234 | letter_opener (~> 1.8, >= 1.8.1) 235 | pry-byebug (~> 3.10, >= 3.10.1) 236 | puma (~> 6.0, >= 6.0.2) 237 | rails (~> 7.0, >= 7.0.4.2) 238 | rexml (~> 3.2, >= 3.2.5) 239 | rubycritic (~> 4.7) 240 | simplecov (~> 0.22.0) 241 | sqlite3 (~> 1.6) 242 | tzinfo-data 243 | 244 | RUBY VERSION 245 | ruby 3.2.0p0 246 | 247 | BUNDLED WITH 248 | 2.2.32 249 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | This Rails app has been intentionally designed in a way that there are areas for improvement. 4 | 5 | It's your mission to find this places and refactor them. 6 | 7 | ## Requirements to run the app 8 | 9 | * Ruby version: `3.2.0` 10 | 11 | * Database: `sqlite3` 12 | 13 | ## How to setup this app 14 | 15 | ```sh 16 | bin/setup 17 | ``` 18 | 19 | ## Table of Contents 20 | 21 | - [Requirements to run the app](#requirements-to-run-the-app) 22 | - [How to setup this app](#how-to-setup-this-app) 23 | - [Table of Contents](#table-of-contents) 24 | - [Useful commands](#useful-commands) 25 | - [Examples of cURL requests to interact with the API](#examples-of-curl-requests-to-interact-with-the-api) 26 | - [Users](#users) 27 | - [Add new user](#add-new-user) 28 | - [Display user](#display-user) 29 | - [Delete user](#delete-user) 30 | - [To-Do Lists](#to-do-lists) 31 | - [Add new to-do list](#add-new-to-do-list) 32 | - [Display to-do list](#display-to-do-list) 33 | - [Display all to-do lists](#display-all-to-do-lists) 34 | - [Edit to-do list](#edit-to-do-list) 35 | - [Remove to-do list](#remove-to-do-list) 36 | - [To-Dos](#to-dos) 37 | - [Add new to-do](#add-new-to-do) 38 | - [Default list](#default-list) 39 | - [In a list](#in-a-list) 40 | - [Display to-do](#display-to-do) 41 | - [Default list](#default-list-1) 42 | - [From a list](#from-a-list) 43 | - [Display all to-dos](#display-all-to-dos) 44 | - [From a list](#from-a-list-1) 45 | - [Edit to-do](#edit-to-do) 46 | - [In a list](#in-a-list-1) 47 | - [Mark to-do as completed](#mark-to-do-as-completed) 48 | - [In a list](#in-a-list-2) 49 | - [Mark to-do as incomplete](#mark-to-do-as-incomplete) 50 | - [In a list](#in-a-list-3) 51 | - [Remove to-do](#remove-to-do) 52 | - [From a list](#from-a-list-2) 53 | 54 | ## Useful commands 55 | 56 | * `bin/rails test` - it will run the test suite. 57 | 58 | * `bin/rails rubycritic` - it will generate a quality report of this codebase. 59 | 60 | ## Examples of cURL requests to interact with the API 61 | 62 | First, run the application: 63 | 64 | ```sh 65 | bin/rails s 66 | ``` 67 | 68 | Then, use some of the following commands to interact with the API resources: 69 | 70 | ### Users 71 | 72 | #### Add new user 73 | 74 | ```sh 75 | curl -X POST "http://localhost:3000/users" \ 76 | -H "Content-Type: application/json" \ 77 | -d '{"user":{"name": "Serradura", "email": "serradura@example.com", "password": "123456", "password_confirmation": "123456"}}' 78 | ``` 79 | 80 | #### Display user 81 | 82 | ```sh 83 | curl -X GET "http://localhost:3000/user" \ 84 | -H "Content-Type: application/json" \ 85 | -H "Authorization: Bearer SOME-USER-TOKEN" 86 | ``` 87 | 88 | #### Delete user 89 | 90 | ```sh 91 | curl -X DELETE "http://localhost:3000/user" \ 92 | -H "Content-Type: application/json" \ 93 | -H "Authorization: Bearer SOME-USER-TOKEN" 94 | ``` 95 | 96 | ### To-Do Lists 97 | 98 | #### Add new to-do list 99 | 100 | ```sh 101 | curl -X POST "http://localhost:3000/todos_lists" \ 102 | -H "Content-Type: application/json" \ 103 | -H "Authorization: Bearer SOME-USER-TOKEN" \ 104 | -d '{"todo":{"title": "Things to learn"}}' 105 | ``` 106 | 107 | #### Display to-do list 108 | 109 | ```sh 110 | curl -X GET "http://localhost:3000/todos_lists/1" \ 111 | -H "Content-Type: application/json" \ 112 | -H "Authorization: Bearer SOME-USER-TOKEN" 113 | ``` 114 | 115 | #### Display all to-do lists 116 | 117 | ```sh 118 | curl -X GET "http://localhost:3000/todo_lists" \ 119 | -H "Content-Type: application/json" \ 120 | -H "Authorization: Bearer SOME-USER-TOKEN" 121 | ``` 122 | 123 | This resource accepts the following query strings: 124 | - sort_by (e.g, 'updated_at') 125 | - order (e.g, 'asc') 126 | 127 | PS: Desc is the default order. 128 | 129 | **Example:** 130 | 131 | ```sh 132 | curl -X GET "http://localhost:3000/todo_lists?sort_by=title" -H "Content-Type: application/json" -H "Authorization: Bearer SOME-USER-TOKEN" 133 | ``` 134 | 135 | #### Edit to-do list 136 | 137 | ```sh 138 | curl -X PUT "http://localhost:3000/todo_lists/1" \ 139 | -H "Content-Type: application/json" \ 140 | -H "Authorization: Bearer SOME-USER-TOKEN" \ 141 | -d '{"todo":{"title": "Things to learn"}}' 142 | ``` 143 | 144 | #### Remove to-do list 145 | 146 | ```sh 147 | curl -X DELETE "http://localhost:3000/todo_lists/1" \ 148 | -H "Content-Type: application/json" \ 149 | -H "Authorization: Bearer SOME-USER-TOKEN" 150 | ``` 151 | 152 | ### To-Dos 153 | 154 | #### Add new to-do 155 | 156 | ##### Default list 157 | 158 | ```sh 159 | curl -X POST "http://localhost:3000/todos" \ 160 | -H "Content-Type: application/json" \ 161 | -H "Authorization: Bearer SOME-USER-TOKEN" \ 162 | -d '{"todo":{"title": "Buy coffee"}}' 163 | ``` 164 | 165 | ##### In a list 166 | 167 | ```sh 168 | curl -X POST "http://localhost:3000/todo_lists/1/todos" \ 169 | -H "Content-Type: application/json" \ 170 | -H "Authorization: Bearer SOME-USER-TOKEN" \ 171 | -d '{"todo":{"title": "Buy coffee"}}' 172 | ``` 173 | 174 | #### Display to-do 175 | 176 | ##### Default list 177 | 178 | ```sh 179 | curl -X GET "http://localhost:3000/todos/1" \ 180 | -H "Content-Type: application/json" \ 181 | -H "Authorization: Bearer SOME-USER-TOKEN" 182 | ``` 183 | 184 | ##### From a list 185 | 186 | ```sh 187 | curl -X GET "http://localhost:3000/todo_lists/1/todos/1" \ 188 | -H "Content-Type: application/json" \ 189 | -H "Authorization: Bearer SOME-USER-TOKEN" 190 | ``` 191 | 192 | #### Display all to-dos 193 | 194 | This resource accepts the following query strings: 195 | - status (e.g, 'completed') 196 | - sort_by (e.g, 'updated_at') 197 | - order (e.g, 'asc') 198 | 199 | PS: Desc is the default order. 200 | 201 | **Example:** 202 | 203 | ```sh 204 | curl -X GET "http://localhost:3000/todos?status=&sort_by=&order=" 205 | -H "Content-Type: application/json" 206 | -H "Authorization: Bearer SOME-USER-TOKEN" 207 | ``` 208 | 209 | The available statuses to filter are: `overdue`, `completed`, `incomplete`. 210 | 211 | ##### From a list 212 | 213 | ```sh 214 | curl -X GET "http://localhost:3000/todo_lists/1/todos/1?status=&sort_by=&order=" \ 215 | -H "Content-Type: application/json" \ 216 | -H "Authorization: Bearer SOME-USER-TOKEN" 217 | ``` 218 | 219 | #### Edit to-do 220 | 221 | Modify the content of the item. 222 | 223 | ```sh 224 | curl -X PUT "http://localhost:3000/todos/1" \ 225 | -H "Content-Type: application/json" \ 226 | -H "Authorization: Bearer SOME-USER-TOKEN" \ 227 | -d '{"todo":{"title": "Buy milk"}}' 228 | ``` 229 | 230 | **Todo params:** 231 | * title: `string` `required`. 232 | * completed: `boolean` `optional`. 233 | 234 | ##### In a list 235 | 236 | ```sh 237 | curl -X PUT "http://localhost:3000/todo_lists/1/todos/1" \ 238 | -H "Content-Type: application/json" \ 239 | -H "Authorization: Bearer SOME-USER-TOKEN" \ 240 | -d '{"todo":{"title": "Buy milk"}}' 241 | ``` 242 | 243 | #### Mark to-do as completed 244 | 245 | Change the status to 'completed'. 246 | 247 | ```sh 248 | curl -X PUT "http://localhost:3000/todos/1/complete" \ 249 | -H "Content-Type: application/json" \ 250 | -H "Authorization: Bearer SOME-USER-TOKEN" 251 | ``` 252 | 253 | ##### In a list 254 | 255 | ```sh 256 | curl -X PUT "http://localhost:3000/todo_lists/1/todos/1/complete" \ 257 | -H "Content-Type: application/json" \ 258 | -H "Authorization: Bearer SOME-USER-TOKEN" 259 | ``` 260 | 261 | #### Mark to-do as incomplete 262 | 263 | Change the status to 'incomplete'. 264 | 265 | ```sh 266 | curl -X PUT "http://localhost:3000/todos/1/incomplete" \ 267 | -H "Content-Type: application/json" \ 268 | -H "Authorization: Bearer SOME-USER-TOKEN" 269 | ``` 270 | 271 | ##### In a list 272 | 273 | ```sh 274 | curl -X PUT "http://localhost:3000/todo_lists/1/todos/1/incomplete" \ 275 | -H "Content-Type: application/json" \ 276 | -H "Authorization: Bearer SOME-USER-TOKEN" 277 | ``` 278 | 279 | #### Remove to-do 280 | 281 | The item will be permanently deleted from the list 282 | 283 | ```sh 284 | curl -X DELETE "http://localhost:3000/todos/1" \ 285 | -H "Content-Type: application/json" \ 286 | -H "Authorization: Bearer SOME-USER-TOKEN" 287 | ``` 288 | 289 | ##### From a list 290 | 291 | ```sh 292 | curl -X DELETE "http://localhost:3000/todo_lists/1/todos/1" \ 293 | -H "Content-Type: application/json" \ 294 | -H "Authorization: Bearer SOME-USER-TOKEN" 295 | ``` 296 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationController < ActionController::API 4 | include ActionController::HttpAuthentication::Token::ControllerMethods 5 | 6 | rescue_from ActionController::ParameterMissing, with: :show_parameter_missing_error 7 | 8 | protected 9 | 10 | def authenticate_user(&block) 11 | return block&.call if current_user 12 | 13 | head :unauthorized 14 | end 15 | 16 | def current_user 17 | @current_user ||= authenticate_with_http_token do |token| 18 | User.find_by(token: token) 19 | end 20 | end 21 | 22 | def render_json(status, json = {}) 23 | render status: status, json: json 24 | end 25 | 26 | def show_parameter_missing_error(exception) 27 | render_json(400, error: exception.message) 28 | end 29 | 30 | def set_todo_lists 31 | user_todo_lists = current_user.todo_lists 32 | 33 | @todo_lists = todo_lists_only_non_default? ? user_todo_lists.non_default : user_todo_lists 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serradura/rails_app_to_refactor/360ca925ade0938cc52b640713744b725d4b370f/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/controllers/todo_lists_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class TodoListsController < ApplicationController 4 | before_action :authenticate_user 5 | 6 | before_action :set_todo_lists 7 | before_action :set_todo_list, except: [:index, :create] 8 | 9 | rescue_from ActiveRecord::RecordNotFound do |not_found| 10 | render_json(404, todo_list: { id: not_found.id, message: 'not found' }) 11 | end 12 | 13 | def index 14 | todo_lists = @todo_lists.order_by(params).map(&:serialize_as_json) 15 | 16 | render_json(200, todo_lists: todo_lists) 17 | end 18 | 19 | def create 20 | todo_list = @todo_lists.create(todo_list_params) 21 | 22 | if todo_list.valid? 23 | render_json(201, todo_list: todo_list.serialize_as_json) 24 | else 25 | render_json(422, todo_list: todo_list.errors.as_json) 26 | end 27 | end 28 | 29 | def show 30 | render_json(200, todo_list: @todo_list.serialize_as_json) 31 | end 32 | 33 | def destroy 34 | @todo_list.destroy 35 | 36 | render_json(200, todo_list: @todo_list.serialize_as_json) 37 | end 38 | 39 | def update 40 | @todo_list.update(todo_list_params) 41 | 42 | if @todo_list.valid? 43 | render_json(200, todo_list: @todo_list.serialize_as_json) 44 | else 45 | render_json(422, todo_list: @todo_list.errors.as_json) 46 | end 47 | end 48 | 49 | private 50 | 51 | def todo_lists_only_non_default? = action_name.in?(['update', 'destroy']) 52 | 53 | def set_todo_list 54 | @todo_list = @todo_lists.find(params[:id]) 55 | end 56 | 57 | def todo_list_params 58 | params.require(:todo_list).permit(:title) 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /app/controllers/todos_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class TodosController < ApplicationController 4 | before_action :authenticate_user 5 | 6 | before_action :set_todo_lists 7 | before_action :set_todos 8 | before_action :set_todo, except: [:index, :create] 9 | 10 | rescue_from ActiveRecord::RecordNotFound do |not_found| 11 | key = not_found.model == 'TodoList' ? :todo_list : :todo 12 | 13 | render_json(404, key => { id: not_found.id, message: 'not found' }) 14 | end 15 | 16 | def index 17 | todos = @todos.filter_by_status(params).order_by(params).map(&:serialize_as_json) 18 | 19 | render_json(200, todos:) 20 | end 21 | 22 | def create 23 | todo = @todos.create(todo_params.except(:completed)) 24 | 25 | if todo.valid? 26 | render_json(201, todo: todo.serialize_as_json) 27 | else 28 | render_json(422, todo: todo.errors.as_json) 29 | end 30 | end 31 | 32 | def show 33 | render_json(200, todo: @todo.serialize_as_json) 34 | end 35 | 36 | def destroy 37 | @todo.destroy 38 | 39 | render_json(200, todo: @todo.serialize_as_json) 40 | end 41 | 42 | def update 43 | @todo.update(todo_params) 44 | 45 | if @todo.valid? 46 | render_json(200, todo: @todo.serialize_as_json) 47 | else 48 | render_json(422, todo: @todo.errors.as_json) 49 | end 50 | end 51 | 52 | def complete 53 | @todo.complete! 54 | 55 | render_json(200, todo: @todo.serialize_as_json) 56 | end 57 | 58 | def incomplete 59 | @todo.incomplete! 60 | 61 | render_json(200, todo: @todo.serialize_as_json) 62 | end 63 | 64 | private 65 | 66 | def todo_lists_only_non_default? = false 67 | 68 | def set_todos 69 | scope = 70 | if params[:todo_list_id].present? 71 | @todo_lists.find(params[:todo_list_id]) 72 | else 73 | action_name == 'create' ? @todo_lists.default.first! : current_user 74 | end 75 | 76 | @todos = scope.todos 77 | end 78 | 79 | def set_todo 80 | @todo = @todos.find(params[:id]) 81 | end 82 | 83 | def todo_params 84 | params.require(:todo).permit(:title, :due_at, :completed) 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /app/controllers/users_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UsersController < ApplicationController 4 | def create 5 | user_params = params.require(:user).permit(:name, :email, :password, :password_confirmation) 6 | 7 | password = user_params[:password].to_s.strip 8 | password_confirmation = user_params[:password_confirmation].to_s.strip 9 | 10 | errors = {} 11 | errors[:password] = ["can't be blank"] if password.blank? 12 | errors[:password_confirmation] = ["can't be blank"] if password_confirmation.blank? 13 | 14 | if errors.present? 15 | render_json(422, user: errors) 16 | else 17 | if password != password_confirmation 18 | render_json(422, user: { password_confirmation: ["doesn't match password"] }) 19 | else 20 | password_digest = Digest::SHA256.hexdigest(password) 21 | 22 | user = User.new( 23 | name: user_params[:name], 24 | email: user_params[:email], 25 | token: SecureRandom.uuid, 26 | password_digest: password_digest 27 | ) 28 | 29 | if user.save 30 | render_json(201, user: user.as_json(only: [:id, :name, :token])) 31 | else 32 | render_json(422, user: user.errors.as_json) 33 | end 34 | end 35 | end 36 | end 37 | 38 | def show 39 | perform_if_authenticated 40 | end 41 | 42 | def destroy 43 | perform_if_authenticated do 44 | current_user.destroy 45 | end 46 | end 47 | 48 | private 49 | 50 | def perform_if_authenticated(&block) 51 | authenticate_user do 52 | block.call if block 53 | 54 | render_json(200, user: { email: current_user.email }) 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationJob < ActiveJob::Base 4 | # Automatically retry jobs that encountered a deadlock 5 | # retry_on ActiveRecord::Deadlocked 6 | 7 | # Most jobs are safe to ignore if the underlying records are no longer available 8 | # discard_on ActiveJob::DeserializationError 9 | end 10 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationMailer < ActionMailer::Base 4 | default from: 'from@example.com' 5 | end 6 | -------------------------------------------------------------------------------- /app/mailers/user_mailer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UserMailer < ApplicationMailer 4 | def welcome 5 | user = params[:user] 6 | 7 | mail( 8 | to: user.email, 9 | body: "Hi #{user.name}, thanks for signing up...", 10 | subject: 'Welcome aboard', 11 | content_type: 'text/plain;charset=UTF-8', 12 | ) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationRecord < ActiveRecord::Base 4 | self.abstract_class = true 5 | end 6 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serradura/rails_app_to_refactor/360ca925ade0938cc52b640713744b725d4b370f/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/todo.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Todo < ApplicationRecord 4 | attr_accessor :completed 5 | 6 | belongs_to :todo_list, required: true, inverse_of: :todos 7 | 8 | has_one :user, through: :todo_list 9 | 10 | scope :overdue, -> { incomplete.where('due_at <= ?', Time.current) } 11 | scope :completed, -> { where.not(completed_at: nil) } 12 | scope :incomplete, -> { where(completed_at: nil) } 13 | 14 | scope :filter_by_status, ->(params) { 15 | case params[:status]&.strip&.downcase 16 | when 'overdue' then overdue 17 | when 'completed' then completed 18 | when 'incomplete' then incomplete 19 | else all 20 | end 21 | } 22 | 23 | scope :order_by, ->(params) { 24 | order = params[:order]&.strip&.downcase == 'asc' ? :asc : :desc 25 | 26 | sort_by = params[:sort_by]&.strip&.downcase 27 | 28 | column_name = column_names.excluding('id').include?(sort_by) ? sort_by : 'id' 29 | 30 | order(column_name => order) 31 | } 32 | 33 | before_validation on: :update do 34 | case completed 35 | when 'true' then complete 36 | when 'false' then incomplete 37 | end 38 | end 39 | 40 | validates :title, presence: true 41 | validates :due_at, presence: true, allow_nil: true 42 | validates :completed_at, presence: true, allow_nil: true 43 | 44 | def overdue? 45 | return false if !due_at || completed_at 46 | 47 | due_at <= Time.current 48 | end 49 | 50 | def incomplete? 51 | completed_at.nil? 52 | end 53 | 54 | def completed? 55 | !incomplete? 56 | end 57 | 58 | def status 59 | return 'completed' if completed? 60 | return 'overdue' if overdue? 61 | 62 | 'incomplete' 63 | end 64 | 65 | def complete 66 | self.completed_at = Time.current unless completed? 67 | end 68 | 69 | def complete! 70 | complete 71 | 72 | self.save if completed_at_changed? 73 | end 74 | 75 | def incomplete 76 | self.completed_at = nil unless incomplete? 77 | end 78 | 79 | def incomplete! 80 | incomplete 81 | 82 | self.save if completed_at_changed? 83 | end 84 | 85 | def serialize_as_json 86 | as_json(except: [:completed], methods: :status) 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /app/models/todo_list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class TodoList < ApplicationRecord 4 | has_many :todos, dependent: :destroy, inverse_of: :todo_list 5 | 6 | belongs_to :user, required: true, inverse_of: :todo_lists 7 | 8 | scope :default, -> { where(default: true) } 9 | scope :non_default, -> { where(default: false) } 10 | 11 | scope :order_by, ->(params) { 12 | order = params[:order]&.strip&.downcase == 'asc' ? :asc : :desc 13 | 14 | sort_by = params[:sort_by]&.strip&.downcase 15 | 16 | column_name = column_names.excluding('id', 'user_id').include?(sort_by) ? sort_by : 'id' 17 | 18 | order(column_name => order) 19 | } 20 | 21 | validates :title, presence: true 22 | validates :default, inclusion: { in: [true, false] } 23 | validate :default_uniqueness 24 | 25 | def serialize_as_json 26 | as_json(except: [:user_id]) 27 | end 28 | 29 | private 30 | 31 | def default_uniqueness 32 | errors.add(:default, 'already exists') if default? && user.todo_lists.default.exists? 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class User < ApplicationRecord 4 | has_many :todo_lists, dependent: :destroy, inverse_of: :user 5 | has_many :todos, through: :todo_lists 6 | 7 | has_one :default_todo_list, ->(user) { user.todo_lists.default }, class_name: 'TodoList' 8 | 9 | validates :name, presence: true 10 | validates :email, presence: true, format: URI::MailTo::EMAIL_REGEXP, uniqueness: true 11 | validates :token, presence: true, length: { is: 36 }, uniqueness: true 12 | validates :password_digest, presence: true, length: { is: 64 } 13 | 14 | after_create :create_default_todo_list 15 | after_commit :send_welcome_email, on: :create 16 | 17 | private 18 | 19 | def create_default_todo_list 20 | todo_lists.create!(title: 'Default', default: true) 21 | end 22 | 23 | def send_welcome_email 24 | UserMailer.with(user: self).welcome.deliver_later 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'bundle' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "rubygems" 12 | 13 | m = Module.new do 14 | module_function 15 | 16 | def invoked_as_script? 17 | File.expand_path($0) == File.expand_path(__FILE__) 18 | end 19 | 20 | def env_var_version 21 | ENV["BUNDLER_VERSION"] 22 | end 23 | 24 | def cli_arg_version 25 | return unless invoked_as_script? # don't want to hijack other binstubs 26 | return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` 27 | bundler_version = nil 28 | update_index = nil 29 | ARGV.each_with_index do |a, i| 30 | if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN 31 | bundler_version = a 32 | end 33 | next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ 34 | bundler_version = $1 || ">= 0.a" 35 | update_index = i 36 | end 37 | bundler_version 38 | end 39 | 40 | def gemfile 41 | gemfile = ENV["BUNDLE_GEMFILE"] 42 | return gemfile if gemfile && !gemfile.empty? 43 | 44 | File.expand_path("../../Gemfile", __FILE__) 45 | end 46 | 47 | def lockfile 48 | lockfile = 49 | case File.basename(gemfile) 50 | when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) 51 | else "#{gemfile}.lock" 52 | end 53 | File.expand_path(lockfile) 54 | end 55 | 56 | def lockfile_version 57 | return unless File.file?(lockfile) 58 | lockfile_contents = File.read(lockfile) 59 | return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ 60 | Regexp.last_match(1) 61 | end 62 | 63 | def bundler_version 64 | @bundler_version ||= begin 65 | env_var_version || cli_arg_version || 66 | lockfile_version || "#{Gem::Requirement.default}.a" 67 | end 68 | end 69 | 70 | def load_bundler! 71 | ENV["BUNDLE_GEMFILE"] ||= gemfile 72 | 73 | # must dup string for RG < 1.8 compatibility 74 | activate_bundler(bundler_version.dup) 75 | end 76 | 77 | def activate_bundler(bundler_version) 78 | if Gem::Version.correct?(bundler_version) && Gem::Version.new(bundler_version).release < Gem::Version.new("2.0") 79 | bundler_version = "< 2" 80 | end 81 | gem_error = activation_error_handling do 82 | gem "bundler", bundler_version 83 | end 84 | return if gem_error.nil? 85 | require_error = activation_error_handling do 86 | require "bundler/version" 87 | end 88 | return if require_error.nil? && Gem::Requirement.new(bundler_version).satisfied_by?(Gem::Version.new(Bundler::VERSION)) 89 | warn "Activating bundler (#{bundler_version}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_version}'`" 90 | exit 42 91 | end 92 | 93 | def activation_error_handling 94 | yield 95 | nil 96 | rescue StandardError, LoadError => e 97 | e 98 | end 99 | end 100 | 101 | m.load_bundler! 102 | 103 | if m.invoked_as_script? 104 | load Gem.bin_path("bundler", "bundle") 105 | end 106 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path("../config/application", __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | # path to your application root. 5 | APP_ROOT = File.expand_path("..", __dir__) 6 | 7 | def system!(*args) 8 | system(*args) || abort("\n== Command #{args} failed ==") 9 | end 10 | 11 | FileUtils.chdir APP_ROOT do 12 | # This script is a way to set up or update your development environment automatically. 13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 14 | # Add necessary setup steps to this file. 15 | 16 | puts "== Installing dependencies ==" 17 | system! "gem install bundler --conservative" 18 | system("bundle check") || system!("bundle install") 19 | 20 | # puts "\n== Copying sample files ==" 21 | # unless File.exist?("config/database.yml") 22 | # FileUtils.cp "config/database.yml.sample", "config/database.yml" 23 | # end 24 | 25 | puts "\n== Preparing database ==" 26 | system! "bin/rails db:prepare" 27 | 28 | puts "\n== Removing old logs and tempfiles ==" 29 | system! "bin/rails log:clear tmp:clear" 30 | 31 | puts "\n== Restarting application server ==" 32 | system! "bin/rails restart" 33 | end 34 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads Spring without using Bundler, in order to be fast. 4 | # It gets overwritten when you run the `spring binstub` command. 5 | 6 | unless defined?(Spring) 7 | require 'rubygems' 8 | require 'bundler' 9 | 10 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) 11 | spring = lockfile.specs.detect { |spec| spec.name == 'spring' } 12 | if spring 13 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path 14 | gem 'spring', spring.version 15 | require 'spring/binstub' 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative 'config/environment' 4 | 5 | run Rails.application 6 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require "rails" 4 | # Pick the frameworks you want: 5 | require "active_model/railtie" 6 | require "active_job/railtie" 7 | require "active_record/railtie" 8 | # require "active_storage/engine" 9 | require "action_controller/railtie" 10 | require "action_mailer/railtie" 11 | # require "action_mailbox/engine" 12 | # require "action_text/engine" 13 | # require "action_view/railtie" 14 | # require "action_cable/engine" 15 | # require "sprockets/railtie" 16 | require "rails/test_unit/railtie" 17 | 18 | # Require the gems listed in Gemfile, including any gems 19 | # you've limited to :test, :development, or :production. 20 | Bundler.require(*Rails.groups) 21 | 22 | module MicroTodoApp 23 | class Application < Rails::Application 24 | # Initialize configuration defaults for originally generated Rails version. 25 | config.load_defaults 7.0 26 | # Settings in config/environments/* take precedence over those specified here. 27 | # Application configuration can go into files in config/initializers 28 | # -- all .rb files in that directory are automatically loaded after loading 29 | # the framework and any gems in your application. 30 | 31 | # Only loads a smaller set of middleware suitable for API only apps. 32 | # Middleware like session, flash, cookies can be added back manually. 33 | # Skip views, helpers and assets when generating a new resource. 34 | config.api_only = true 35 | 36 | config.active_job.queue_adapter = :async 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 2 | 3 | require "bundler/setup" # Set up gems listed in the Gemfile. 4 | require "bootsnap/setup" # Speed up boot time by caching expensive operations. 5 | -------------------------------------------------------------------------------- /config/credentials.yml.enc: -------------------------------------------------------------------------------- 1 | GgbTbFNWhKi+hpV3XX3DYdVR/x818R3KD9t+bZ8vmhY28brrmRdk389hFgUVCe4hqbId+ekfer9BEvcVD89mgOSWhjs5YvQ0QC0Ve8TkSqOvyATLBG1BKVYSkuXsoies04SksdloJIlfj9DRiSix02ab38e7lPPr0ih7moKXwgplB9JfhUKPIq1sPUecdmN9gmhedEZOE1URDkbqvWCkVRnYA2Q34TrQosS6i8FNNH0VOMlX0myn2EKzM1I2/iFztG6B3BEMgpK4wKtHc84sKdFrrSrMHmQA3bcU4FV0kP7qZI9hi0avlgUlA+9BIvOs7Syyan62MagHy9IPxg1plbgMtxmpjT+IL9hiMwjSGHOeqGI5Yr8plK5oU+MGXsAZNeaPosFy0fKGPlDCr89bjWPkngD8goBYa0zx--LC6/i3c78dd/x4gz--I+V5tLUgZgjtP8Hnz+IpaA== -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite. Versions 3.8.0 and up are supported. 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/test.sqlite3 22 | 23 | production: 24 | <<: *default 25 | database: db/production.sqlite3 26 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # In the development environment your application's code is reloaded any time 7 | # it changes. This slows down response time but is perfect for development 8 | # since you don't have to restart the web server when you make code changes. 9 | config.cache_classes = false 10 | 11 | # Do not eager load code on boot. 12 | config.eager_load = false 13 | 14 | # Show full error reports. 15 | config.consider_all_requests_local = true 16 | 17 | # Enable server timing 18 | config.server_timing = true 19 | 20 | # Enable/disable caching. By default caching is disabled. 21 | # Run rails dev:cache to toggle caching. 22 | if Rails.root.join("tmp/caching-dev.txt").exist? 23 | config.cache_store = :memory_store 24 | config.public_file_server.headers = { 25 | "Cache-Control" => "public, max-age=#{2.days.to_i}" 26 | } 27 | else 28 | config.action_controller.perform_caching = false 29 | 30 | config.cache_store = :null_store 31 | end 32 | 33 | # Don't care if the mailer can't send. 34 | config.action_mailer.raise_delivery_errors = false 35 | 36 | config.action_mailer.perform_caching = false 37 | 38 | 39 | config.action_mailer.delivery_method = :letter_opener 40 | 41 | # Print deprecation notices to the Rails logger. 42 | config.active_support.deprecation = :log 43 | 44 | # Raise exceptions for disallowed deprecations. 45 | config.active_support.disallowed_deprecation = :raise 46 | 47 | # Tell Active Support which deprecation messages to disallow. 48 | config.active_support.disallowed_deprecation_warnings = [] 49 | 50 | # Raise an error on page load if there are pending migrations. 51 | config.active_record.migration_error = :page_load 52 | 53 | # Highlight code that triggered database queries in logs. 54 | config.active_record.verbose_query_logs = true 55 | 56 | 57 | # Raises error for missing translations. 58 | # config.i18n.raise_on_missing_translations = true 59 | 60 | # Annotate rendered view with file names. 61 | # config.action_view.annotate_rendered_view_with_filenames = true 62 | 63 | # Uncomment if you wish to allow Action Cable access from any origin. 64 | # config.action_cable.disable_request_forgery_protection = true 65 | end 66 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # Code is not reloaded between requests. 7 | config.cache_classes = true 8 | 9 | # Eager load code on boot. This eager loads most of Rails and 10 | # your application in memory, allowing both threaded web servers 11 | # and those relying on copy on write to perform better. 12 | # Rake tasks automatically ignore this option for performance. 13 | config.eager_load = true 14 | 15 | # Full error reports are disabled and caching is turned on. 16 | config.consider_all_requests_local = false 17 | 18 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] 19 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). 20 | # config.require_master_key = true 21 | 22 | # Disable serving static files from the `/public` folder by default since 23 | # Apache or NGINX already handles this. 24 | config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present? 25 | 26 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 27 | # config.asset_host = "http://assets.example.com" 28 | 29 | # Specifies the header that your server uses for sending files. 30 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache 31 | # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX 32 | 33 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 34 | # config.force_ssl = true 35 | 36 | # Include generic and useful information about system operation, but avoid logging too much 37 | # information to avoid inadvertent exposure of personally identifiable information (PII). 38 | config.log_level = :info 39 | 40 | # Prepend all log lines with the following tags. 41 | config.log_tags = [ :request_id ] 42 | 43 | # Use a different cache store in production. 44 | # config.cache_store = :mem_cache_store 45 | 46 | # Use a real queuing backend for Active Job (and separate queues per environment). 47 | # config.active_job.queue_adapter = :resque 48 | # config.active_job.queue_name_prefix = "micro_todo_app_production" 49 | 50 | config.action_mailer.perform_caching = false 51 | 52 | # Ignore bad email addresses and do not raise email delivery errors. 53 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 54 | # config.action_mailer.raise_delivery_errors = false 55 | 56 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 57 | # the I18n.default_locale when a translation cannot be found). 58 | config.i18n.fallbacks = true 59 | 60 | # Don't log any deprecations. 61 | config.active_support.report_deprecations = false 62 | 63 | # Use default logging formatter so that PID and timestamp are not suppressed. 64 | config.log_formatter = ::Logger::Formatter.new 65 | 66 | # Use a different logger for distributed setups. 67 | # require "syslog/logger" 68 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name") 69 | 70 | if ENV["RAILS_LOG_TO_STDOUT"].present? 71 | logger = ActiveSupport::Logger.new(STDOUT) 72 | logger.formatter = config.log_formatter 73 | config.logger = ActiveSupport::TaggedLogging.new(logger) 74 | end 75 | 76 | # Do not dump schema after migrations. 77 | config.active_record.dump_schema_after_migration = false 78 | end 79 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # The test environment is used exclusively to run your application's 2 | # test suite. You never need to work with it otherwise. Remember that 3 | # your test database is "scratch space" for the test suite and is wiped 4 | # and recreated between test runs. Don't rely on the data there! 5 | 6 | Rails.application.configure do 7 | # Settings specified here will take precedence over those in config/application.rb. 8 | 9 | config.cache_classes = false 10 | 11 | # Do not eager load code on boot. This avoids loading your whole application 12 | # just for the purpose of running a single test. If you are using a tool that 13 | # preloads Rails for running tests, you may have to set it to true. 14 | config.eager_load = false 15 | 16 | # Configure public file server for tests with Cache-Control for performance. 17 | config.public_file_server.enabled = true 18 | config.public_file_server.headers = { 19 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}" 20 | } 21 | 22 | # Show full error reports and disable caching. 23 | config.consider_all_requests_local = true 24 | config.action_controller.perform_caching = false 25 | config.cache_store = :null_store 26 | 27 | # Raise exceptions instead of rendering exception templates. 28 | config.action_dispatch.show_exceptions = false 29 | 30 | # Disable request forgery protection in test environment. 31 | config.action_controller.allow_forgery_protection = false 32 | 33 | # Store uploaded files on the local file system in a temporary directory. 34 | # config.active_storage.service = :test 35 | 36 | # config.action_mailer.perform_caching = false 37 | 38 | # Tell Action Mailer not to deliver emails to the real world. 39 | # The :test delivery method accumulates sent emails in the 40 | # ActionMailer::Base.deliveries array. 41 | # config.action_mailer.delivery_method = :test 42 | 43 | # Print deprecation notices to the stderr. 44 | config.active_support.deprecation = :stderr 45 | 46 | # Raises error for missing translations. 47 | # config.action_view.raise_on_missing_translations = true 48 | end 49 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /config/initializers/cors.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Avoid CORS issues when API is called from the frontend app. 4 | # Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests. 5 | 6 | # Read more: https://github.com/cyu/rack-cors 7 | 8 | # Rails.application.config.middleware.insert_before 0, Rack::Cors do 9 | # allow do 10 | # origins "example.com" 11 | # 12 | # resource "*", 13 | # headers: :any, 14 | # methods: [:get, :post, :put, :patch, :delete, :options, :head] 15 | # end 16 | # end 17 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, "\\1en" 8 | # inflect.singular /^(ox)en/i, "\\1" 9 | # inflect.irregular "person", "people" 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym "RESTful" 16 | # end 17 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at https://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # This configuration file will be evaluated by Puma. The top-level methods that 2 | # are invoked here are part of Puma's configuration DSL. For more information 3 | # about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. 4 | 5 | # Puma can serve each request in a thread from an internal thread pool. 6 | # The `threads` method setting takes two numbers: a minimum and maximum. 7 | # Any libraries that use thread pools should be configured to match 8 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 9 | # and maximum; this matches the default thread size of Active Record. 10 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 11 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } 12 | threads min_threads_count, max_threads_count 13 | 14 | # Specifies that the worker count should equal the number of processors in production. 15 | if ENV["RAILS_ENV"] == "production" 16 | worker_count = Integer(ENV.fetch("WEB_CONCURRENCY") { Concurrent.physical_processor_count }) 17 | workers worker_count if worker_count > 1 18 | end 19 | 20 | # Specifies the `worker_timeout` threshold that Puma will use to wait before 21 | # terminating a worker in development environments. 22 | worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" 23 | 24 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 25 | port ENV.fetch("PORT") { 3000 } 26 | 27 | # Specifies the `environment` that Puma will run in. 28 | environment ENV.fetch("RAILS_ENV") { "development" } 29 | 30 | # Specifies the `pidfile` that Puma will use. 31 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } 32 | 33 | # Allow puma to be restarted by `bin/rails restart` command. 34 | plugin :tmp_restart 35 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.routes.draw do 4 | resources :users, only: [:create] 5 | 6 | resource :user, only: [:show, :destroy] 7 | 8 | resources :todos do 9 | member do 10 | put 'complete' 11 | put 'incomplete' 12 | end 13 | end 14 | 15 | resources :todo_lists do 16 | resources :todos do 17 | member do 18 | put 'complete' 19 | put 'incomplete' 20 | end 21 | end 22 | end 23 | # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html 24 | end 25 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | Spring.watch( 2 | ".ruby-version", 3 | ".rbenv-vars", 4 | "tmp/restart.txt", 5 | "tmp/caching-dev.txt" 6 | ) 7 | -------------------------------------------------------------------------------- /db/migrate/20191203023223_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration[6.0] 2 | def change 3 | create_table :users do |t| 4 | t.string :name, null: false 5 | t.string :email, null: false 6 | t.string :password_digest, null: false 7 | t.string :token, index: true 8 | 9 | t.timestamps 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20191203035409_create_todos.rb: -------------------------------------------------------------------------------- 1 | class CreateTodos < ActiveRecord::Migration[6.0] 2 | def change 3 | create_table :todos do |t| 4 | t.string :title 5 | t.datetime :due_at, index: true 6 | t.datetime :completed_at, index: true 7 | t.references :user, null: false, foreign_key: true 8 | 9 | t.timestamps 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20230208115402_create_todo_lists.rb: -------------------------------------------------------------------------------- 1 | class CreateTodoLists < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :todo_lists do |t| 4 | t.string :title 5 | t.boolean :default, default: false, null: false 6 | t.references :user, null: false, foreign_key: true 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20230208115941_add_todo_list_id_to_todos.rb: -------------------------------------------------------------------------------- 1 | class AddTodoListIdToTodos < ActiveRecord::Migration[7.0] 2 | def change 3 | remove_reference(:todos, :user) 4 | 5 | add_reference(:todos, :todo_list, null: false, index: true, foreign_key: true) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # This file is the source Rails uses to define your schema when running `bin/rails 6 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to 7 | # be faster and is potentially less error prone than running all of your 8 | # migrations from scratch. Old migrations may fail to apply correctly if those 9 | # migrations use external dependencies or application code. 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema[7.0].define(version: 2023_02_08_115941) do 14 | create_table "todo_lists", force: :cascade do |t| 15 | t.string "title" 16 | t.boolean "default", default: false, null: false 17 | t.integer "user_id", null: false 18 | t.datetime "created_at", null: false 19 | t.datetime "updated_at", null: false 20 | t.index ["user_id"], name: "index_todo_lists_on_user_id" 21 | end 22 | 23 | create_table "todos", force: :cascade do |t| 24 | t.string "title" 25 | t.datetime "due_at", precision: nil 26 | t.datetime "completed_at", precision: nil 27 | t.datetime "created_at", null: false 28 | t.datetime "updated_at", null: false 29 | t.integer "todo_list_id", null: false 30 | t.index ["completed_at"], name: "index_todos_on_completed_at" 31 | t.index ["due_at"], name: "index_todos_on_due_at" 32 | t.index ["todo_list_id"], name: "index_todos_on_todo_list_id" 33 | end 34 | 35 | create_table "users", force: :cascade do |t| 36 | t.string "name", null: false 37 | t.string "email", null: false 38 | t.string "password_digest", null: false 39 | t.string "token" 40 | t.datetime "created_at", null: false 41 | t.datetime "updated_at", null: false 42 | t.index ["token"], name: "index_users_on_token" 43 | end 44 | 45 | add_foreign_key "todo_lists", "users" 46 | add_foreign_key "todos", "todo_lists" 47 | end 48 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) 7 | # Character.create(name: 'Luke', movie: movies.first) 8 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serradura/rails_app_to_refactor/360ca925ade0938cc52b640713744b725d4b370f/lib/tasks/.keep -------------------------------------------------------------------------------- /lib/tasks/rubycritic.rake: -------------------------------------------------------------------------------- 1 | require "rubycritic/rake_task" 2 | 3 | RubyCritic::RakeTask.new do |task| 4 | # Glob pattern to match source files. Defaults to FileList['.']. 5 | task.paths = FileList['app/**/*.rb', 'lib/**/*.rb'] 6 | end 7 | -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serradura/rails_app_to_refactor/360ca925ade0938cc52b640713744b725d4b370f/log/.keep -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /test/controllers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serradura/rails_app_to_refactor/360ca925ade0938cc52b640713744b725d4b370f/test/controllers/.keep -------------------------------------------------------------------------------- /test/controllers/todo_lists_controller/create_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TodoListsControllerCreateTest < ActionDispatch::IntegrationTest 4 | concerning :Helpers do 5 | included do 6 | include TodoListAssertions::JSON 7 | include TodoListAssertions::Response 8 | end 9 | 10 | def create_todo_list(user: nil, params: {}) 11 | post(todo_lists_url, params:, headers: AuthenticationHeader.from(user)) 12 | end 13 | end 14 | 15 | test "responds with 401 when user token is invalid" do 16 | # Act 17 | create_todo_list 18 | 19 | # Assert 20 | assert_response 401 21 | end 22 | 23 | test "responds with 400 when todo_list params are missing" do 24 | # Arrange 25 | user = users(:without_todos) 26 | 27 | # Act 28 | create_todo_list(user:, params: { title: 'Things to learn' }) 29 | 30 | # Assert 31 | assert_response 400 32 | 33 | assert_equal( 34 | { "error" => "param is missing or the value is empty: todo_list" }, 35 | JSON.parse(response.body) 36 | ) 37 | end 38 | 39 | test "responds with 422 when invalid params are received" do 40 | # Arrange 41 | user = users(:without_todos) 42 | 43 | # Act 44 | create_todo_list(user:, params: { todo_list: { title: '' } }) 45 | 46 | # Assert 47 | assert_todo_list_unprocessable_entity(response:, errors: { "title"=>["can't be blank"] }) 48 | end 49 | 50 | test "responds with 201 when valid params are received" do 51 | # Arrange 52 | user = users(:without_todos) 53 | 54 | # Act 55 | create_todo_list(user:, params: { todo_list: { title: 'Things to learn' } }) 56 | 57 | # Assert 58 | assert_response 201 59 | 60 | json = JSON.parse(response.body) 61 | 62 | assert user.todo_lists.where(id: json.dig('todo_list', 'id')).exists? 63 | 64 | assert_hash_schema({ "todo_list" => Hash }, json) 65 | 66 | assert_todo_list_json(json["todo_list"]) 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /test/controllers/todo_lists_controller/destroy_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TodoListsControllerDestroyTest < ActionDispatch::IntegrationTest 4 | concerning :Helpers do 5 | included do 6 | include TodoListAssertions::JSON 7 | include TodoListAssertions::Response 8 | end 9 | 10 | def destroy_todo_list(id:, user: nil) 11 | delete(todo_list_url(id: id), headers: AuthenticationHeader.from(user)) 12 | end 13 | end 14 | 15 | test "responds with 401 when user token is invalid" do 16 | # Act 17 | destroy_todo_list(id: 1) 18 | 19 | # Assert 20 | assert_response 401 21 | end 22 | 23 | test "responds with 404 when the todo list cannot be found" do 24 | # Arrange 25 | user = users(:without_todos) 26 | 27 | # Act 28 | destroy_todo_list(user:, id: 1) 29 | 30 | # Assert 31 | assert_todo_list_not_found(response:, id: '1') 32 | end 33 | 34 | test "responds with 404 when trying to delete the default todo list" do 35 | # Arrange 36 | user = users(:john_doe) 37 | 38 | id = user.default_todo_list.id 39 | 40 | # Act 41 | destroy_todo_list(user:, id:) 42 | 43 | # Assert 44 | assert_todo_list_not_found(response:, id:) 45 | end 46 | 47 | test "responds with 200 after deleting an existing todo list" do 48 | # Arrange 49 | todo_list = todo_lists(:john_doe_non_default) 50 | 51 | # Act 52 | assert_difference(-> { todo_list.user.todo_lists.count }, -1) do 53 | destroy_todo_list(user: todo_list.user, id: todo_list.id) 54 | end 55 | 56 | # Assert 57 | assert_response 200 58 | 59 | json = JSON.parse(response.body) 60 | 61 | assert_hash_schema({ "todo_list" => Hash }, json) 62 | 63 | assert_todo_list_json(json["todo_list"]) 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /test/controllers/todo_lists_controller/index_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TodoListsControllerIndexTest < ActionDispatch::IntegrationTest 4 | concerning :Helpers do 5 | included do 6 | include TodoListAssertions::JSON 7 | include TodoListAssertions::Response 8 | end 9 | 10 | def get_todo_lists(user: nil, params: {}) 11 | get(todo_lists_url, params:, headers: AuthenticationHeader.from(user)) 12 | end 13 | 14 | def assert_todo_lists_json_schema(json) 15 | assert_hash_schema({"todo_lists" => Array}, json) 16 | 17 | json["todo_lists"].each { |item| assert_todo_list_json(item, default: item['default']) } 18 | end 19 | 20 | def assert_todo_lists_json_order(relation, response, **sort_by) 21 | json = response.is_a?(Hash) ? response : JSON.parse(response.body) 22 | 23 | assert_todo_lists_json_schema(json) 24 | 25 | todo_lists = relation.is_a?(User) ? relation.todo_lists : relation 26 | 27 | assert_equal( 28 | todo_lists.order(sort_by).map(&:id), 29 | json['todo_lists'].map { |todo_list| todo_list['id'] }, 30 | sort_by.inspect 31 | ) 32 | end 33 | 34 | def get_and_assert_todos_order(user, column_name:) 35 | prepare_param = ->(str) do 36 | [str.capitalize, str.upcase, str].sample.then { [" #{_1}", " #{_1} ", " #{_1} "].sample } 37 | end 38 | 39 | sort_by = prepare_param.(column_name) 40 | 41 | ## 42 | # DEFAULT 43 | # 44 | get_todo_lists(user:, params: { sort_by: }) 45 | 46 | assert_response 200 47 | 48 | assert_todo_lists_json_order(user, response, column_name => :desc) 49 | 50 | ## 51 | # DESC 52 | # 53 | get_todo_lists(user:, params: { sort_by:, order: prepare_param.('desc') }) 54 | 55 | assert_response 200 56 | 57 | assert_todo_lists_json_order(user, response, column_name => :desc) 58 | 59 | ## 60 | # ASC 61 | # 62 | get_todo_lists(user:, params: { sort_by:, order: prepare_param.('asc') }) 63 | 64 | assert_response 200 65 | 66 | assert_todo_lists_json_order(user, response, column_name => :asc) 67 | end 68 | end 69 | 70 | test "responds with 401 when the user token is invalid" do 71 | # Act 72 | get_todo_lists 73 | 74 | # Assert 75 | assert_response 401 76 | end 77 | 78 | test "responds with 200 even when the user only the default to-do lists" do 79 | # Arrange 80 | user = users(:without_todos) 81 | 82 | # Act 83 | get_todo_lists(user:) 84 | 85 | # Assert 86 | assert_response 200 87 | 88 | json = JSON.parse(response.body) 89 | 90 | assert_todo_lists_json_schema(json) 91 | 92 | assert_equal(1, json["todo_lists"].size) 93 | end 94 | 95 | test "responds with 200 when the user has to-do lists and no filter status is applied" do 96 | # Arrange 97 | user = users(:john_doe) 98 | 99 | # Act 100 | get_todo_lists(user:) 101 | 102 | # Assert 103 | assert_response 200 104 | 105 | json = JSON.parse(response.body) 106 | 107 | assert_todo_lists_json_schema(json) 108 | 109 | assert_equal(2, json["todo_lists"].size) 110 | end 111 | 112 | test "responds with 200 when the user has to-do lists and the sort_by is set to 'title'" do 113 | # Arrange 114 | user = users(:john_doe) 115 | 116 | # Act & Assert 117 | get_and_assert_todos_order(user, column_name: 'title') 118 | end 119 | 120 | test "responds with 200 when the user has to-do lists and the sort_by is set to 'created_at'" do 121 | # Arrange 122 | user = users(:john_doe) 123 | 124 | # Act & Assert 125 | get_and_assert_todos_order(user, column_name: 'created_at') 126 | end 127 | 128 | test "responds with 200 when the user has to-do lists and the sort_by is set to 'updated_at'" do 129 | # Arrange 130 | user = users(:john_doe) 131 | 132 | # Act & Assert 133 | get_and_assert_todos_order(user, column_name: 'updated_at') 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /test/controllers/todo_lists_controller/show_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TodoListsControllerShowTest < ActionDispatch::IntegrationTest 4 | concerning :Helpers do 5 | included do 6 | include TodoListAssertions::JSON 7 | include TodoListAssertions::Response 8 | end 9 | 10 | def get_todo_list(id:, user: nil) 11 | get(todo_list_url(id: id), headers: AuthenticationHeader.from(user)) 12 | end 13 | end 14 | 15 | test "responds with 401 when user token is invalid" do 16 | # Act 17 | get_todo_list(id: 1) 18 | 19 | # Assert 20 | assert_response 401 21 | end 22 | 23 | test "responds with 404 when the todo list cannot be found" do 24 | # Arrange 25 | id = todo_lists(:john_doe_non_default).id 26 | 27 | user = users(:without_todos) 28 | 29 | # Act 30 | get_todo_list(user:, id:) 31 | 32 | # Assert 33 | assert_todo_list_not_found(response:, id:) 34 | end 35 | 36 | test "responds with 200 after finding the todo list" do 37 | # Arrange 38 | todo_list = todo_lists(:john_doe_default) 39 | 40 | # Act 41 | get_todo_list(user: todo_list.user, id: todo_list.id) 42 | 43 | # Assert 44 | assert_response 200 45 | 46 | json = JSON.parse(response.body) 47 | 48 | assert_equal(todo_list.id, json.dig("todo_list", "id")) 49 | 50 | assert_hash_schema({ "todo_list" => Hash }, json) 51 | 52 | assert_todo_list_json(json["todo_list"], default: true) 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test/controllers/todo_lists_controller/update_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TodoListsControllerUpdateTest < ActionDispatch::IntegrationTest 4 | concerning :Helpers do 5 | included do 6 | include TodoListAssertions::JSON 7 | include TodoListAssertions::Response 8 | end 9 | 10 | def update_todo_list(id:, user: nil, params: {}) 11 | put(todo_list_url(id: id), params:, headers: AuthenticationHeader.from(user)) 12 | end 13 | end 14 | 15 | test "responds with 401 when user token is invalid" do 16 | # Act 17 | update_todo_list(id: 1) 18 | 19 | # Assert 20 | assert_response 401 21 | end 22 | 23 | test "responds with 400 when to-do list parameters are missing" do 24 | # Arrange 25 | todo_list = todo_lists(:john_doe_non_default) 26 | 27 | # Act 28 | update_todo_list(user: todo_list.user, id: todo_list, params: {}) 29 | 30 | # Assert 31 | assert_response 400 32 | 33 | assert_equal( 34 | { "error" => "param is missing or the value is empty: todo_list" }, 35 | JSON.parse(response.body) 36 | ) 37 | end 38 | 39 | test "responds with 404 when to-do list not found" do 40 | # Arrange 41 | id = todo_lists(:john_doe_non_default).id 42 | 43 | user = users(:without_todos) 44 | 45 | # Act 46 | update_todo_list(user:, id: id, params: { title: 'Things to do' }) 47 | 48 | # Assert 49 | assert_todo_list_not_found(response:, id:) 50 | end 51 | 52 | test "responds with 404 when trying to update the default todo list" do 53 | # Arrange 54 | user = users(:john_doe) 55 | 56 | id = user.default_todo_list.id 57 | 58 | # Act 59 | update_todo_list(user:, id: id, params: { title: 'Things to do' }) 60 | 61 | # Assert 62 | assert_todo_list_not_found(response:, id:) 63 | end 64 | 65 | test "responds with 422 when invalid parameters are received" do 66 | # Arrange 67 | todo_list = todo_lists(:john_doe_non_default) 68 | 69 | # Act 70 | update_todo_list(user: todo_list.user, id: todo_list, params: { todo_list: { title: '' } }) 71 | 72 | # Assert 73 | assert_todo_list_unprocessable_entity(response:, errors: { "title"=>["can't be blank"] }) 74 | end 75 | 76 | test "responds with 200 when a valid title is received" do 77 | # Arrange 78 | todo_list = todo_lists(:john_doe_non_default) 79 | 80 | # Act 81 | update_todo_list(user: todo_list.user, id: todo_list, params: { todo_list: { title: 'Things to buy' } }) 82 | 83 | # Assert 84 | assert_response 200 85 | 86 | json = JSON.parse(response.body) 87 | 88 | todo_list_found = todo_list.user.todo_lists.find(json.dig('todo_list', 'id')) 89 | 90 | assert_equal(todo_list.id, todo_list_found.id) 91 | 92 | assert_equal('Things to buy', todo_list_found.title) 93 | 94 | assert_hash_schema({ "todo_list" => Hash }, json) 95 | 96 | assert_todo_list_json(json["todo_list"], default: false) 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /test/controllers/todos_controller/complete_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TodosControllerCompleteTest < ActionDispatch::IntegrationTest 4 | concerning :Helpers do 5 | included do 6 | include TodoAssertions::JSON 7 | include TodoAssertions::Response 8 | include TodoListAssertions::Response 9 | end 10 | 11 | def complete_todo(id:, user: nil, todo_list_id: nil) 12 | url = todo_list_id ? complete_todo_list_todo_url(todo_list_id:, id:) : complete_todo_url(id:) 13 | 14 | put(url, headers: AuthenticationHeader.from(user)) 15 | end 16 | end 17 | 18 | test "responds with 401 when user token is invalid" do 19 | # Act 20 | complete_todo(id: 1) 21 | 22 | # Assert 23 | assert_response 401 24 | end 25 | 26 | test "responds with 404 when the todo cannot be found" do 27 | # Arrange 28 | user = users(:without_todos) 29 | 30 | # Act 31 | complete_todo(user:, id: 1) 32 | 33 | # Assert 34 | assert_todo_not_found(response:, id: '1') 35 | end 36 | 37 | test "responds with 404 when the requested to-do does not belong to the user" do 38 | # Arrange 39 | todo = todos(:john_doe_incomplete) 40 | 41 | todo_from_another_user = users(:without_todos).default_todo_list.todos.create(title: 'New task') 42 | 43 | # Act 44 | complete_todo(user: todo.user, id: todo_from_another_user.id) 45 | 46 | # Assert 47 | assert_todo_not_found(response:, id: todo_from_another_user.id) 48 | end 49 | 50 | test "responds with 200 after marking an existing todo as completed" do 51 | # Arrange 52 | todo = todos(:john_doe_incomplete) 53 | 54 | # Act 55 | complete_todo(user: todo.user, id: todo) 56 | 57 | # Assert 58 | assert_response 200 59 | 60 | todo.reload 61 | 62 | assert_predicate(todo, :completed?) 63 | 64 | json = JSON.parse(response.body) 65 | 66 | assert_hash_schema({ "todo" => Hash }, json) 67 | 68 | assert_todo_json(json["todo"]) 69 | end 70 | 71 | ## 72 | # Nested resource: /todo_lists/:todo_list_id/todos/:id/complete 73 | # 74 | test "responds with 404 when the todo list cannot be found" do 75 | # Arrange 76 | user = users(:john_doe) 77 | 78 | # Act 79 | complete_todo(user:, id: user.todos.first, todo_list_id: 1) 80 | 81 | # Assert 82 | assert_todo_list_not_found(response:, id: '1') 83 | end 84 | 85 | test "responds with 404 when the todo is not found in the specified todo list" do 86 | # Arrange 87 | todo = todos(:john_doe_incomplete) 88 | 89 | todo_list_id = todo_lists(:john_doe_non_default).id 90 | 91 | # Act 92 | complete_todo(user: todo.user, todo_list_id:, id: todo.id) 93 | 94 | # Assert 95 | assert_todo_not_found(response:, id: todo.id) 96 | end 97 | 98 | test "responds with 200 after marking an existing todo as completed from a specific todo list" do 99 | # Arrange 100 | todo = todos(:john_doe_incomplete) 101 | 102 | todo_list_id = todo.todo_list 103 | 104 | # Act 105 | complete_todo(user: todo.user, todo_list_id:, id: todo) 106 | 107 | # Assert 108 | assert_response 200 109 | 110 | todo.reload 111 | 112 | assert_predicate(todo, :completed?) 113 | 114 | json = JSON.parse(response.body) 115 | 116 | assert_hash_schema({ "todo" => Hash }, json) 117 | 118 | assert_todo_json(json["todo"]) 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /test/controllers/todos_controller/create_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TodosControllerCreateTest < ActionDispatch::IntegrationTest 4 | concerning :Helpers do 5 | included do 6 | include TodoAssertions::JSON 7 | include TodoAssertions::Response 8 | include TodoListAssertions::Response 9 | end 10 | 11 | def create_todo(user: nil, params: {}, todo_list_id: nil) 12 | url = todo_list_id ? todo_list_todos_url(todo_list_id) : todos_url 13 | 14 | post(url, params:, headers: AuthenticationHeader.from(user)) 15 | end 16 | end 17 | 18 | test "responds with 401 when user token is invalid" do 19 | # Act 20 | create_todo 21 | 22 | # Assert 23 | assert_response 401 24 | end 25 | 26 | test "responds with 400 when todo params are missing" do 27 | # Arrange 28 | user = users(:without_todos) 29 | 30 | # Act 31 | create_todo(user:, params: { title: 'Buy coffee' }) 32 | 33 | # Assert 34 | assert_response 400 35 | 36 | assert_todo_bad_request(response:) 37 | end 38 | 39 | test "responds with 422 when invalid params are received" do 40 | # Arrange 41 | user = users(:without_todos) 42 | 43 | # Act 44 | create_todo(user:, params: { todo: { title: '' } }) 45 | 46 | # Assert 47 | assert_todo_unprocessable_entity(response:, errors: { "title"=>["can't be blank"] }) 48 | end 49 | 50 | test "responds with 201 when valid params are received" do 51 | # Arrange 52 | user = users(:without_todos) 53 | 54 | # Act 55 | create_todo(user:, params: { todo: { title: 'Buy coffee' } }) 56 | 57 | # Assert 58 | assert_response 201 59 | 60 | json = JSON.parse(response.body) 61 | 62 | relation = Todo.where(id: json.dig('todo', 'id')) 63 | 64 | assert_predicate(relation, :exists?) 65 | 66 | assert_hash_schema({ "todo" => Hash }, json) 67 | 68 | assert_todo_json(json["todo"]) 69 | end 70 | 71 | ## 72 | # Nested resource: /todo_lists/:todo_list_id/todos 73 | # 74 | test "responds with 404 when the todo list cannot be found" do 75 | # Arrange 76 | user = users(:john_doe) 77 | 78 | todo_list_id = 1 79 | 80 | # Act 81 | create_todo(user:, todo_list_id:, params: { todo: { title: 'Buy coffee' } }) 82 | 83 | # Assert 84 | assert_todo_list_not_found(response:, id: '1') 85 | end 86 | 87 | test "responds with 404 when the todo list does not belong to the user" do 88 | # Arrange 89 | user = users(:john_doe) 90 | 91 | todo_list_id = todo_lists(:without_items).id 92 | 93 | # Act 94 | create_todo(user:, todo_list_id: , params: { todo: { title: 'Buy coffee' } }) 95 | 96 | # Assert 97 | assert_todo_list_not_found(response:, id: todo_list_id) 98 | end 99 | 100 | test "responds with 201 when valid params are received for create a todo in a todo list" do 101 | # Arrange 102 | user = users(:john_doe) 103 | 104 | todo_list_id = todo_lists(:john_doe_non_default).id 105 | 106 | # Act 107 | create_todo(user:, todo_list_id:, params: { todo: { title: 'Buy coffee' } }) 108 | 109 | # Assert 110 | assert_response 201 111 | 112 | json = JSON.parse(response.body) 113 | 114 | todo = Todo.find(json.dig('todo', 'id')) 115 | 116 | assert_equal(todo_list_id, todo.todo_list_id) 117 | 118 | assert_hash_schema({ "todo" => Hash }, json) 119 | 120 | assert_todo_json(json["todo"]) 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /test/controllers/todos_controller/destroy_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TodosControllerDestroyTest < ActionDispatch::IntegrationTest 4 | concerning :Helpers do 5 | included do 6 | include TodoAssertions::JSON 7 | include TodoAssertions::Response 8 | include TodoListAssertions::Response 9 | end 10 | 11 | def destroy_todo(id:, user: nil, todo_list_id: nil) 12 | url = todo_list_id ? todo_list_todo_url(todo_list_id:, id:) : todo_url(id:) 13 | 14 | delete(url, headers: AuthenticationHeader.from(user)) 15 | end 16 | end 17 | 18 | test "responds with 401 when user token is invalid" do 19 | # Act 20 | destroy_todo(id: 1) 21 | 22 | # Assert 23 | assert_response 401 24 | end 25 | 26 | test "responds with 404 when the todo cannot be found" do 27 | # Arrange 28 | user = users(:without_todos) 29 | 30 | # Act 31 | destroy_todo(user:, id: 1) 32 | 33 | # Assert 34 | assert_todo_not_found(response:, id: '1') 35 | end 36 | 37 | test "responds with 404 when the requested to-do does not belong to the user" do 38 | # Arrange 39 | todo = todos(:john_doe_incomplete) 40 | 41 | todo_from_another_user = users(:without_todos).default_todo_list.todos.create(title: 'New task') 42 | 43 | # Act 44 | destroy_todo(user: todo.user, id: todo_from_another_user.id) 45 | 46 | # Assert 47 | assert_todo_not_found(response:, id: todo_from_another_user.id) 48 | end 49 | 50 | test "responds with 200 after deleting an existing todo" do 51 | # Arrange 52 | todo = todos(:john_doe_incomplete) 53 | 54 | # Act 55 | assert_difference(-> { todo.user.todos.count }, -1) do 56 | destroy_todo(user: todo.user, id: todo) 57 | end 58 | 59 | # Assert 60 | assert_response 200 61 | 62 | json = JSON.parse(response.body) 63 | 64 | assert_hash_schema({ "todo" => Hash }, json) 65 | 66 | assert_todo_json(json["todo"]) 67 | end 68 | 69 | ## 70 | # Nested resource: /todo_lists/:todo_list_id/todo/:id 71 | # 72 | test "responds with 404 when the todo list cannot be found" do 73 | # Arrange 74 | user = users(:john_doe) 75 | 76 | todo_list_id = 1 77 | 78 | # Act 79 | destroy_todo(user:, todo_list_id:, id: 1) 80 | 81 | # Assert 82 | assert_todo_list_not_found(response:, id: '1') 83 | end 84 | 85 | test "responds with 404 when the todo list does not belong to the user" do 86 | # Arrange 87 | user = users(:john_doe) 88 | 89 | todo_list_id = todo_lists(:without_items).id 90 | 91 | # Act 92 | destroy_todo(user:, todo_list_id:, id: user.todos.first.id) 93 | 94 | # Assert 95 | assert_todo_list_not_found(response:, id: todo_list_id) 96 | end 97 | 98 | test "responds with 200 after deleting a todo item from a specific todo list" do 99 | # Arrange 100 | todo = todos(:john_doe_incomplete) 101 | 102 | todo_list_id = todo.todo_list.id 103 | 104 | # Act 105 | assert_difference(-> { todo.user.todos.count }, -1) do 106 | destroy_todo(user: todo.user, todo_list_id:, id: todo) 107 | end 108 | 109 | # Assert 110 | assert_response 200 111 | 112 | json = JSON.parse(response.body) 113 | 114 | assert_hash_schema({ "todo" => Hash }, json) 115 | 116 | assert_todo_json(json["todo"]) 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /test/controllers/todos_controller/incomplete_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TodosControllerUncompleteTest < ActionDispatch::IntegrationTest 4 | concerning :Helpers do 5 | included do 6 | include TodoAssertions::JSON 7 | include TodoAssertions::Response 8 | include TodoListAssertions::Response 9 | end 10 | 11 | def incomplete_todo(id:, user: nil, todo_list_id: nil) 12 | url = todo_list_id ? incomplete_todo_list_todo_url(todo_list_id:, id:) : incomplete_todo_url(id:) 13 | 14 | put(url, headers: AuthenticationHeader.from(user)) 15 | end 16 | 17 | def todo_from_another_user 18 | @todo_from_another_user ||= 19 | users(:without_todos).default_todo_list.todos.create(title: 'New todo') 20 | end 21 | end 22 | 23 | test "responds with 401 when user token is invalid" do 24 | # Act 25 | incomplete_todo(id: 1) 26 | 27 | # Assert 28 | assert_response 401 29 | end 30 | 31 | test "responds with 404 when the todo cannot be found" do 32 | # Arrange 33 | user = users(:without_todos) 34 | 35 | # Act 36 | incomplete_todo(user:, id: 1) 37 | 38 | # Assert 39 | assert_todo_not_found(response:, id: '1') 40 | end 41 | 42 | test "responds with 404 when the requested to-do does not belong to the user" do 43 | # Arrange 44 | todo = todos(:john_doe_completed) 45 | 46 | # Act 47 | incomplete_todo(user: todo.user, id: todo_from_another_user.id) 48 | 49 | # Assert 50 | assert_todo_not_found(response:, id: todo_from_another_user.id) 51 | end 52 | 53 | test "responds with 200 after marking an existing todo as incomplete" do 54 | # Arrange 55 | todo = todos(:john_doe_completed) 56 | 57 | # Act 58 | incomplete_todo(user: todo.user, id: todo.id) 59 | 60 | # Assert 61 | assert_response 200 62 | 63 | todo.reload 64 | 65 | assert_predicate(todo, :incomplete?) 66 | 67 | json = JSON.parse(response.body) 68 | 69 | assert_hash_schema({ "todo" => Hash }, json) 70 | 71 | assert_todo_json(json["todo"]) 72 | end 73 | 74 | ## 75 | # Nested resource: /todo_lists/:todo_list_id/todos/:id/incomplete 76 | # 77 | test "responds with 404 when the todo list cannot be found" do 78 | # Arrange 79 | user = users(:john_doe) 80 | 81 | # Act 82 | incomplete_todo(user:, id: user.todos.first, todo_list_id: 1) 83 | 84 | # Assert 85 | assert_todo_list_not_found(response:, id: '1') 86 | end 87 | 88 | test "responds with 404 when the todo is not found in the specified todo list" do 89 | # Arrange 90 | todo = todos(:john_doe_completed) 91 | 92 | todo_list_id = todo_lists(:john_doe_non_default).id 93 | 94 | # Act 95 | incomplete_todo(user: todo.user, todo_list_id:, id: todo) 96 | 97 | # Assert 98 | assert_todo_not_found(response:, id: todo.id) 99 | end 100 | 101 | test "responds with 200 after marking an existing todo as incomplete from a specific todo list" do 102 | # Arrange 103 | todo = todos(:john_doe_completed) 104 | 105 | todo_list_id = todo.todo_list.id 106 | 107 | # Act 108 | incomplete_todo(user: todo.user, todo_list_id:, id: todo.id) 109 | 110 | # Assert 111 | assert_response 200 112 | 113 | todo.reload 114 | 115 | assert_predicate(todo, :incomplete?) 116 | 117 | json = JSON.parse(response.body) 118 | 119 | assert_equal(todo_list_id, todo.todo_list_id) 120 | 121 | assert_hash_schema({ "todo" => Hash }, json) 122 | 123 | assert_todo_json(json["todo"]) 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /test/controllers/todos_controller/index_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TodosControllerIndexTest < ActionDispatch::IntegrationTest 4 | concerning :Helpers do 5 | included do 6 | include TodoAssertions::JSON 7 | include TodoAssertions::Response 8 | include TodoListAssertions::Response 9 | end 10 | 11 | def get_todos(user: nil, todo_list_id: nil, params: {}) 12 | url = todo_list_id ? todo_list_todos_url(todo_list_id) : todos_url 13 | 14 | get(url, params:, headers: AuthenticationHeader.from(user)) 15 | end 16 | 17 | def assert_todos_json_schema(json) 18 | assert_hash_schema({"todos" => Array}, json) 19 | 20 | json["todos"].each { |item| assert_todo_json(item) } 21 | end 22 | 23 | def assert_todos_json_order(relation, response, todo_list_id, **sort_by) 24 | json = response.is_a?(Hash) ? response : JSON.parse(response.body) 25 | 26 | assert_todos_json_schema(json) 27 | 28 | todos = relation.is_a?(User) ? relation.todos : relation 29 | todos = todos.where(todo_list_id: todo_list_id) if todo_list_id 30 | 31 | assert_equal( 32 | todos.order(sort_by).map(&:id), 33 | json['todos'].map { |todo| todo['id'] }, 34 | sort_by.inspect 35 | ) 36 | end 37 | 38 | def get_and_assert_todos_order(user, column_name:, todo_list_id: nil) 39 | prepare_param = ->(str) do 40 | [str.capitalize, str.upcase, str].sample.then { [" #{_1}", " #{_1} ", " #{_1} "].sample } 41 | end 42 | 43 | sort_by = prepare_param.(column_name) 44 | 45 | ## 46 | # DEFAULT 47 | # 48 | get_todos(user:, todo_list_id:, params: { sort_by: }) 49 | 50 | assert_response 200 51 | 52 | assert_todos_json_order(user, response, todo_list_id, column_name => :desc) 53 | 54 | ## 55 | # DESC 56 | # 57 | get_todos(user:, todo_list_id:, params: { sort_by:, order: prepare_param.('desc') }) 58 | 59 | assert_response 200 60 | 61 | assert_todos_json_order(user, response, todo_list_id, column_name => :desc) 62 | 63 | ## 64 | # ASC 65 | # 66 | get_todos(user:, todo_list_id:, params: { sort_by:, order: prepare_param.('asc') }) 67 | 68 | assert_response 200 69 | 70 | assert_todos_json_order(user, response, todo_list_id, column_name => :asc) 71 | end 72 | end 73 | 74 | test "responds with 401 when the user token is invalid" do 75 | get_todos 76 | 77 | assert_response 401 78 | end 79 | 80 | test "responds with 200 even when the user has no to-dos" do 81 | user = users(:without_todos) 82 | 83 | get_todos(user:, params: {}) 84 | 85 | assert_response 200 86 | 87 | assert_equal( 88 | { "todos" => [] }, 89 | JSON.parse(response.body) 90 | ) 91 | end 92 | 93 | test "responds with 200 when the user has to-dos and no filter status is applied" do 94 | user = users(:john_doe) 95 | 96 | get_todos(user:, params: {}) 97 | 98 | assert_response 200 99 | 100 | json = JSON.parse(response.body) 101 | 102 | assert_todos_json_schema(json) 103 | 104 | assert_equal(4, json["todos"].size) 105 | end 106 | 107 | test "responds with 200 when the user has to-dos and the filter status is set to 'incomplete'" do 108 | user = users(:john_doe) 109 | 110 | get_todos(user:, params: { status: 'incomplete' }) 111 | 112 | assert_response 200 113 | 114 | json = JSON.parse(response.body) 115 | 116 | assert_todos_json_schema(json) 117 | 118 | assert_equal(2, json["todos"].size) 119 | end 120 | 121 | test "responds with 200 when the user has to-dos and the filter status is set to 'completed'" do 122 | user = users(:john_doe) 123 | 124 | get_todos(user:, params: { status: 'completed' }) 125 | 126 | assert_response 200 127 | 128 | json = JSON.parse(response.body) 129 | 130 | assert_todos_json_schema(json) 131 | 132 | assert_equal(2, json["todos"].size) 133 | 134 | assert(json["todos"].all? { |todo| todo['completed_at'].present? }) 135 | end 136 | 137 | test "responds with 200 when the user has to-dos and the filter status is set to 'overdue'" do 138 | user = users(:john_doe) 139 | 140 | get_todos(user:, params: { status: 'overdue' }) 141 | 142 | assert_response 200 143 | 144 | json = JSON.parse(response.body) 145 | 146 | assert_todos_json_schema(json) 147 | 148 | assert_equal(1, json["todos"].size) 149 | 150 | assert(json["todos"].all? { |todo| todo['completed_at'].blank? }) 151 | end 152 | 153 | test "responds with 200 when the user has to-dos and the sort_by is set to 'title'" do 154 | user = users(:john_doe) 155 | 156 | get_and_assert_todos_order(user, column_name: 'title') 157 | end 158 | 159 | test "responds with 200 when the user has to-dos and the sort_by is set to 'due_at'" do 160 | user = users(:john_doe) 161 | 162 | get_and_assert_todos_order(user, column_name: 'due_at') 163 | end 164 | 165 | test "responds with 200 when the user has to-dos and the sort_by is set to 'completed_at'" do 166 | user = users(:john_doe) 167 | 168 | get_and_assert_todos_order(user, column_name: 'completed_at') 169 | end 170 | 171 | test "responds with 200 when the user has to-dos and the sort_by is set to 'created_at'" do 172 | user = users(:john_doe) 173 | 174 | get_and_assert_todos_order(user, column_name: 'created_at') 175 | end 176 | 177 | test "responds with 200 when the user has to-dos and the sort_by is set to 'updated_at'" do 178 | user = users(:john_doe) 179 | 180 | get_and_assert_todos_order(user, column_name: 'updated_at') 181 | end 182 | 183 | test "responds with 200 when the user has to-dos and the sort_by is set to 'todo_list_id'" do 184 | user = users(:john_doe) 185 | 186 | user.todo_lists.non_default.first.todos.create(title: 'New todo') 187 | 188 | get_and_assert_todos_order(user, column_name: 'todo_list_id') 189 | end 190 | 191 | test "responds with 200 when the user has to-dos and receive the status and sort_by params" do 192 | user = users(:john_doe) 193 | 194 | get_todos(user:, params: { status: 'completed', sort_by: 'title' }) 195 | 196 | assert_response 200 197 | 198 | json = JSON.parse(response.body) 199 | 200 | todo_list_id = nil 201 | 202 | assert_todos_json_order(user.todos.completed, json, todo_list_id, title: :desc) 203 | 204 | assert_equal(2, json["todos"].size) 205 | end 206 | 207 | ## 208 | # Nested resource: /todo_lists/:todo_list_id/todos 209 | # 210 | test "responds with 404 when the todo list cannot be found" do 211 | user = users(:john_doe) 212 | 213 | get_todos(user:, todo_list_id: 1) 214 | 215 | assert_todo_list_not_found(response:, id: '1') 216 | end 217 | 218 | test "responds with 200 when the user requests a specific todo list" do 219 | user = users(:john_doe) 220 | 221 | todos = user.todo_lists.non_default.first.todos 222 | 223 | todo = todos.create(title: 'New todo') 224 | 225 | get_todos(user:, todo_list_id: todo.todo_list_id) 226 | 227 | assert_response 200 228 | 229 | json = JSON.parse(response.body) 230 | 231 | assert_todos_json_schema(json) 232 | 233 | assert_equal(1, json["todos"].size) 234 | end 235 | 236 | test "responds with 200 when the user requests a specific todo list and uses some filtering" do 237 | todo_list = todo_lists(:john_doe_non_default) 238 | 239 | todo_list.todos.create(title: 'New todo 2') 240 | todo_list.todos.create(title: 'New todo 1').tap(&:complete!) 241 | todo_list.todos.create(title: 'New todo 3') 242 | 243 | user = todo_list.user 244 | 245 | ## 246 | # Check the sorting 247 | # 248 | get_and_assert_todos_order(user, todo_list_id: todo_list.id, column_name: 'title') 249 | 250 | ## 251 | # Check the filtering by status 252 | # 253 | get_todos(user:, todo_list_id: todo_list.id, params: { status: 'completed' }) 254 | 255 | assert_response 200 256 | 257 | json2 = JSON.parse(response.body) 258 | 259 | assert_equal(1, json2["todos"].size) 260 | end 261 | end 262 | -------------------------------------------------------------------------------- /test/controllers/todos_controller/show_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TodosControllerShowTest < ActionDispatch::IntegrationTest 4 | concerning :Helpers do 5 | included do 6 | include TodoAssertions::JSON 7 | include TodoAssertions::Response 8 | include TodoListAssertions::Response 9 | end 10 | 11 | def get_todo(id:, todo_list_id: nil, user: nil) 12 | url = todo_list_id ? todo_list_todo_url(todo_list_id:, id:) : todo_url(id:) 13 | 14 | get(url, headers: AuthenticationHeader.from(user)) 15 | end 16 | end 17 | 18 | test "responds with 401 when user token is invalid" do 19 | # Act 20 | get_todo(id: 1) 21 | 22 | # Assert 23 | assert_response 401 24 | end 25 | 26 | test "responds with 404 when the todo cannot be found" do 27 | # Arrange 28 | user = users(:without_todos) 29 | 30 | # Act 31 | get_todo(user:, id: 1) 32 | 33 | # Assert 34 | assert_todo_not_found(response:, id: '1') 35 | end 36 | 37 | 38 | test "responds with 404 when the requested to-do does not belong to the user" do 39 | # Arrange 40 | todo = todos(:john_doe_incomplete) 41 | 42 | todo_from_another_user = users(:without_todos).default_todo_list.todos.create(title: 'New task') 43 | 44 | # Act 45 | get_todo(user: todo.user, id: todo_from_another_user.id) 46 | 47 | # Assert 48 | assert_todo_not_found(response:, id: todo_from_another_user.id) 49 | end 50 | 51 | test "responds with 200 after finding the todo" do 52 | # Arrange 53 | todo = todos(:john_doe_incomplete) 54 | 55 | # Act 56 | get_todo(user: todo.user, id: todo.id) 57 | 58 | # Assert 59 | assert_response 200 60 | 61 | json = JSON.parse(response.body) 62 | 63 | assert_equal(todo.id, json.dig("todo", "id")) 64 | 65 | assert_hash_schema({ "todo" => Hash }, json) 66 | 67 | assert_todo_json(json["todo"]) 68 | end 69 | 70 | ## 71 | # Nested resource: /todo_lists/:todo_list_id/todo/:id 72 | # 73 | test "responds with 404 when the todo list cannot be found" do 74 | # Arrange 75 | user = users(:john_doe) 76 | 77 | todo_list_id = 1 78 | 79 | # Act 80 | get_todo(user:, todo_list_id:, id: 1) 81 | 82 | # Assert 83 | assert_todo_list_not_found(response:, id: '1') 84 | end 85 | 86 | test "responds with 404 when the todo list does not belong to the user" do 87 | # Arrange 88 | user = users(:john_doe) 89 | 90 | todo_list_id = todo_lists(:without_items).id 91 | 92 | # Act 93 | get_todo(user:, todo_list_id:, id: user.todos.first.id) 94 | 95 | # Assert 96 | assert_todo_list_not_found(response:, id: todo_list_id) 97 | end 98 | 99 | test "responds with 200 after finding a todo item in a specific todo list" do 100 | # Arrange 101 | todo = todos(:john_doe_incomplete) 102 | 103 | todo_list_id = todo.todo_list.id 104 | 105 | # Act 106 | get_todo(user: todo.user, todo_list_id:, id: todo) 107 | 108 | # Assert 109 | assert_response 200 110 | 111 | json = JSON.parse(response.body) 112 | 113 | assert_equal(todo.id, json.dig("todo", "id")) 114 | 115 | assert_hash_schema({ "todo" => Hash }, json) 116 | 117 | assert_todo_json(json["todo"]) 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /test/controllers/todos_controller/update_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TodosControllerUpdateTest < ActionDispatch::IntegrationTest 4 | concerning :Helpers do 5 | included do 6 | include TodoAssertions::JSON 7 | include TodoAssertions::Response 8 | include TodoListAssertions::Response 9 | end 10 | 11 | def update_todo(id:, todo_list_id: nil, user: nil, params: {}) 12 | url = todo_list_id ? todo_list_todo_url(todo_list_id:, id:) : todo_url(id:) 13 | 14 | put(url, params: params, headers: AuthenticationHeader.from(user)) 15 | end 16 | end 17 | 18 | test "responds with 401 when user token is invalid" do 19 | # Act 20 | update_todo(id: 1) 21 | 22 | # Assert 23 | assert_response 401 24 | end 25 | 26 | test "responds with 400 when to-do parameters are missing" do 27 | # Arrange 28 | todo = todos(:john_doe_incomplete) 29 | 30 | # Act 31 | update_todo(user: todo.user, id: todo, params: { title: 'Buy coffee' }) 32 | 33 | # Assert 34 | assert_response 400 35 | 36 | assert_todo_bad_request(response:) 37 | end 38 | 39 | test "responds with 404 when to-do not found" do 40 | # Arrange 41 | user = users(:without_todos) 42 | 43 | # Act 44 | update_todo(user: user, id: 1, params: { title: 'Buy coffee' }) 45 | 46 | # Assert 47 | assert_todo_not_found(response:, id: '1') 48 | end 49 | 50 | test "responds with 404 when the requested to-do does not belong to the user" do 51 | # Arrange 52 | todo = todos(:john_doe_incomplete) 53 | 54 | todo_from_another_user = users(:without_todos).default_todo_list.todos.create(title: 'New task') 55 | 56 | # Act 57 | update_todo(user: todo.user, id: todo_from_another_user.id, params: { todo: { title: 'Buy coffee' } }) 58 | 59 | # Assert 60 | assert_todo_not_found(response:, id: todo_from_another_user.id) 61 | end 62 | 63 | test "responds with 422 when invalid parameters are received" do 64 | # Arrange 65 | todo = todos(:john_doe_incomplete) 66 | 67 | # Act 68 | update_todo(user: todo.user, id: todo, params: { todo: { title: '' } }) 69 | 70 | # Assert 71 | assert_todo_unprocessable_entity(response:, errors: { "title"=>["can't be blank"] }) 72 | end 73 | 74 | test "responds with 200 when a valid title is received" do 75 | # Arrange 76 | todo = todos(:john_doe_incomplete) 77 | 78 | first_title = todo.title 79 | second_title = 'Buy coffee' 80 | 81 | # Act 82 | update_todo(user: todo.user, id: todo, params: { todo: { title: second_title } }) 83 | 84 | # Assert 85 | assert_response 200 86 | 87 | json = JSON.parse(response.body) 88 | 89 | todo_found = Todo.find(json.dig('todo', 'id')) 90 | 91 | assert_equal(todo.id, todo_found.id) 92 | assert_equal(second_title, todo_found.title) 93 | 94 | assert_todo_json(json["todo"]) 95 | 96 | assert_predicate(json["todo"]["completed_at"], :blank?) 97 | end 98 | 99 | test "responds with 200 when a valid completed param is received" do 100 | # Arrange 101 | todo = todos(:john_doe_incomplete) 102 | 103 | # Act 104 | update_todo(user: todo.user, id: todo, params: { todo: { completed: true } }) 105 | 106 | # Assert 107 | assert_response 200 108 | 109 | json = JSON.parse(response.body) 110 | 111 | todo_found = Todo.find(json.dig('todo', 'id')) 112 | 113 | assert_equal(todo.id, todo_found.id) 114 | assert_predicate(todo_found, :completed?) 115 | 116 | assert_todo_json(json["todo"]) 117 | 118 | assert_predicate(json["todo"]["completed_at"], :present?) 119 | end 120 | 121 | test "responds with 200 when a valid title and completed params are received" do 122 | # Arrange 123 | todo = todos(:john_doe_completed) 124 | 125 | # Act 126 | update_todo(user: todo.user, id: todo, params: { todo: { title: 'Buy tea', completed: false } }) 127 | 128 | # Assert 129 | assert_response 200 130 | 131 | json = JSON.parse(response.body) 132 | 133 | todo_found = Todo.find(json.dig('todo', 'id')) 134 | 135 | assert_equal(todo.id, todo_found.id) 136 | assert_equal('Buy tea', todo_found.title) 137 | assert_predicate(todo_found, :incomplete?) 138 | 139 | assert_todo_json(json["todo"]) 140 | 141 | assert_equal('Buy tea', json["todo"]["title"]) 142 | 143 | assert_predicate(json["todo"]["completed_at"], :blank?) 144 | end 145 | 146 | ## 147 | # Nested resource: /todo_lists/:todo_list_id/todo/:id 148 | # 149 | test "responds with 404 when the todo list cannot be found" do 150 | # Arrange 151 | user = users(:john_doe) 152 | 153 | todo_list_id = 1 154 | 155 | # Act 156 | update_todo(user:, todo_list_id:, id: 1, params: { todo: { title: 'Buy tea' } }) 157 | 158 | # Assert 159 | assert_todo_list_not_found(response:, id: 1) 160 | end 161 | 162 | test "responds with 404 when the todo list does not belong to the user" do 163 | # Arrange 164 | user = users(:john_doe) 165 | 166 | todo = todos(:john_doe_incomplete) 167 | 168 | todo_list_id = todo_lists(:without_items).id 169 | 170 | # Act 171 | update_todo(user:, todo_list_id:, id: todo.id, params: { todo: { title: 'Buy tea' } }) 172 | 173 | # Assert 174 | assert_todo_list_not_found(response:, id: todo_list_id) 175 | end 176 | 177 | test "responds with 200 after updating a todo item from a specific todo list" do 178 | # Arrange 179 | todo = todos(:john_doe_incomplete) 180 | 181 | todo_list_id = todo.todo_list.id 182 | 183 | previous_title = todo.title 184 | 185 | # Act 186 | update_todo(user: todo.user, todo_list_id:, id: todo, params: { todo: { title: 'Buy tea', completed: false } }) 187 | 188 | # Assert 189 | assert_response 200 190 | 191 | json = JSON.parse(response.body) 192 | 193 | todo_found = Todo.find(json.dig('todo', 'id')) 194 | 195 | assert_equal(todo.id, todo_found.id) 196 | assert_predicate(todo_found, :incomplete?) 197 | 198 | refute_equal(previous_title, todo_found.title) 199 | assert_equal('Buy tea', todo_found.title) 200 | 201 | assert_todo_json(json["todo"]) 202 | 203 | assert_predicate(json["todo"]["completed_at"], :blank?) 204 | end 205 | end 206 | -------------------------------------------------------------------------------- /test/controllers/users_controller/create_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UsersControllerCreateTest < ActionDispatch::IntegrationTest 4 | test "responds with 400 when the user parameter is missing" do 5 | # Act 6 | post users_url 7 | 8 | # Assert 9 | assert_response 400 10 | 11 | assert_equal( 12 | { "error" => "param is missing or the value is empty: user" }, 13 | JSON.parse(response.body) 14 | ) 15 | end 16 | 17 | test "responds with 400 when password parameters are absent" do 18 | # Act 19 | post users_url, params: { user: { password: '' } } 20 | 21 | # Assert 22 | assert_response 422 23 | 24 | assert_equal( 25 | { 26 | "user" => { 27 | "password" => ["can't be blank"], 28 | "password_confirmation" => ["can't be blank"] 29 | } 30 | }, 31 | JSON.parse(response.body) 32 | ) 33 | end 34 | 35 | test "responds with 400 when the password confirmation does not match the password" do 36 | # Act 37 | post users_url, params: { user: { password: '123', password_confirmation: '321' } } 38 | 39 | # Assert 40 | assert_response 422 41 | 42 | assert_equal( 43 | { "user" => { "password_confirmation" => ["doesn't match password"] } }, 44 | JSON.parse(response.body) 45 | ) 46 | end 47 | 48 | test "responds with 422 when user data is invalid" do 49 | # Act 50 | post users_url, params: { user: { password: '123', password_confirmation: '123', name: '' } } 51 | 52 | # Assert 53 | assert_response 422 54 | 55 | assert_equal( 56 | {"user" => { 57 | "name" => ["can't be blank"], 58 | "email"=>["can't be blank", "is invalid"] 59 | }}, 60 | JSON.parse(response.body) 61 | ) 62 | end 63 | 64 | test "responds with 201 after successfully creating the user" do 65 | # Arrange 66 | user_params = { user: { 67 | name: 'Serradura', 68 | email: 'serradura@gmail.com', 69 | password: '123', 70 | password_confirmation: '123' } } 71 | 72 | # Act 73 | assert_difference 'User.count', +1 do 74 | assert_enqueued_emails 1 do 75 | post(users_url, params: user_params) 76 | end 77 | end 78 | 79 | # Assert 80 | assert_response 201 81 | 82 | json = JSON.parse(response.body) 83 | 84 | user_id = json.dig("user", "id") 85 | 86 | relation = User.where(id: user_id) 87 | 88 | # FACT: A user will be persisted. 89 | assert_predicate(relation, :exists?) 90 | 91 | # FACT: The JSON response will have the user's token. 92 | assert_hash_schema({ 93 | "id" => Integer, 94 | "name" => "Serradura", 95 | "token" => RegexpPatterns::UUID 96 | }, json["user"]) 97 | 98 | # FACT: An email will be sent after the user creation. 99 | job = ActiveJob::Base.queue_adapter.enqueued_jobs.first 100 | 101 | assert_equal("ActionMailer::MailDeliveryJob", job["job_class"]) 102 | 103 | assert_equal("UserMailer#welcome", job['arguments'][0..1].join('#')) 104 | 105 | job_user_gid = GlobalID.parse(job['arguments'].last.dig("params", "user", "_aj_globalid")) 106 | 107 | assert_equal(user_id.to_s, job_user_gid.model_id) 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /test/controllers/users_controller/destroy_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UsersControllerDestroyTest < ActionDispatch::IntegrationTest 4 | test "responds with 401 when the user token is invalid" do 5 | # Act 6 | delete user_url 7 | 8 | # Assert 9 | assert_response 401 10 | end 11 | 12 | test "responds with 200 when the user token is valid" do 13 | # Arrange 14 | user = users(:john_doe) 15 | 16 | assert user.todos.any? 17 | 18 | # Act 19 | delete user_url, headers: AuthenticationHeader.from(user) 20 | 21 | # Assert 22 | assert_response 200 23 | 24 | assert_equal( 25 | { "user" => { "email" => "john.doe@example.com" } }, 26 | JSON.parse(response.body) 27 | ) 28 | 29 | refute user.todos.any? 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/controllers/users_controller/show_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UsersControllerShowTest < ActionDispatch::IntegrationTest 4 | test "responds with 401 when the user token is invalid" do 5 | # Act 6 | get user_url 7 | 8 | # Assert 9 | assert_response 401 10 | end 11 | 12 | test "responds with 200 when the user token is valid" do 13 | # Arrange 14 | user = users(:john_doe) 15 | 16 | # Act 17 | get user_url, headers: AuthenticationHeader.from(user) 18 | 19 | # Assert 20 | assert_response 200 21 | 22 | assert_equal( 23 | { "user" => { "email" => "john.doe@example.com" } }, 24 | JSON.parse(response.body) 25 | ) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/fixtures/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serradura/rails_app_to_refactor/360ca925ade0938cc52b640713744b725d4b370f/test/fixtures/.keep -------------------------------------------------------------------------------- /test/fixtures/files/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serradura/rails_app_to_refactor/360ca925ade0938cc52b640713744b725d4b370f/test/fixtures/files/.keep -------------------------------------------------------------------------------- /test/fixtures/todo_lists.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | john_doe_default: 4 | title: Default 5 | default: true 6 | user: john_doe 7 | 8 | john_doe_non_default: 9 | title: Things to learn 10 | default: false 11 | user: john_doe 12 | 13 | without_items: 14 | title: Default 15 | default: true 16 | user: without_todos 17 | -------------------------------------------------------------------------------- /test/fixtures/todos.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | john_doe_incomplete: 4 | title: Buy milk 5 | completed_at: null 6 | due_at: null 7 | todo_list: john_doe_default 8 | 9 | john_doe_completed: 10 | title: Buy bread 11 | completed_at: <%= Time.current.to_formatted_s(:db) %> 12 | due_at: null 13 | todo_list: john_doe_default 14 | 15 | john_doe_completed_and_overdue: 16 | title: Buy bread 17 | completed_at: <%= Time.current.to_formatted_s(:db) %> 18 | due_at: <%= 2.days.ago.to_formatted_s(:db) %> 19 | todo_list: john_doe_default 20 | 21 | john_doe_overdue: 22 | title: Buy fruits 23 | completed_at: null 24 | due_at: <%= 1.day.ago.to_formatted_s(:db) %> 25 | todo_list: john_doe_default 26 | -------------------------------------------------------------------------------- /test/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | john_doe: 4 | name: John Doe 5 | email: john.doe@example.com 6 | token: 802b5310-005b-4770-895a-37a31286b709 7 | password_digest: # 123456 8 | 8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92 9 | 10 | without_todos: 11 | name: Rodrigo 12 | email: rodrigo@example.com 13 | token: a00830d5-0ae5-4cb2-a81f-899565f82d76 14 | password_digest: # 654321 15 | 481f6cc0511143ccdd7e2d1b1b94faf0a700a8b49cd13922a70b5ae28acaa8c5 16 | -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serradura/rails_app_to_refactor/360ca925ade0938cc52b640713744b725d4b370f/test/integration/.keep -------------------------------------------------------------------------------- /test/mailers/user_mailer_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UserMailerTest < ActionMailer::TestCase 4 | test "welcome" do 5 | user = users(:john_doe) 6 | 7 | email = UserMailer.with(user: user).welcome 8 | 9 | assert_emails 1 do 10 | email.deliver_now 11 | end 12 | 13 | assert_equal(["from@example.com"], email.from) 14 | assert_equal(["john.doe@example.com"], email.to) 15 | assert_equal("Welcome aboard", email.subject) 16 | assert_equal("Hi John Doe, thanks for signing up...", email.body.to_s) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serradura/rails_app_to_refactor/360ca925ade0938cc52b640713744b725d4b370f/test/models/.keep -------------------------------------------------------------------------------- /test/models/todo_list_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class TodoListTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/todo_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TodoTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/user_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UserTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/support/authentication_header.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AuthenticationHeader 4 | def self.from(user) 5 | user ? { 'Authorization' => "Bearer token=\"#{user.token}\"" } : {} 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/support/hash_schema_assertions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module HashSchemaAssertions 4 | def assert_hash_schema!(spec, hash) 5 | keys_diff = spec.keys - hash.keys 6 | 7 | assert_equal([], keys_diff, "expected to not have these: #{keys_diff}") 8 | 9 | spec.each do |key, expected| 10 | value = hash[key] 11 | error_message = "The key #{key.inspect} has an invalid value" 12 | 13 | case expected 14 | when Proc then assert(expected.call(value), error_message) 15 | when Module then assert_kind_of(expected, value, error_message) 16 | when Regexp then assert_match(expected, value, error_message) 17 | when NilClass then assert_nil(value, error_message) 18 | else assert_equal(expected, value, error_message) 19 | end 20 | end 21 | end 22 | 23 | def assert_hash_schema(spec, hash) 24 | keys_diff = hash.keys - spec.keys 25 | 26 | assert_equal([], keys_diff, "expected to have these keys: #{keys_diff}") 27 | 28 | assert_hash_schema!(spec, hash) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/support/regexp_patterns.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RegexpPatterns 4 | UUID = /\b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b/ 5 | 6 | ISO8601_DATETIME = /\A\d{4}(-\d\d(-\d\d(T\d\d:\d\d(:\d\d)?(\.\d+)?(([+-]\d\d:\d\d)|Z)?)?)?)?\z/i 7 | end 8 | -------------------------------------------------------------------------------- /test/support/todo_assertions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TodoAssertions 4 | module JSON 5 | Nil_Or_An_ISO8601_Datetime = -> value do 6 | value.nil? ? true : value.match?(RegexpPatterns::ISO8601_DATETIME) 7 | end 8 | 9 | def assert_todo_json(json) 10 | assert_hash_schema({ 11 | "id" => Integer, 12 | "title" => String, 13 | "status" => /\A(overdue|completed|incomplete)\z/, 14 | "due_at" => Nil_Or_An_ISO8601_Datetime, 15 | "completed_at" => Nil_Or_An_ISO8601_Datetime, 16 | "created_at" => RegexpPatterns::ISO8601_DATETIME, 17 | "updated_at" => RegexpPatterns::ISO8601_DATETIME, 18 | "todo_list_id" => Integer 19 | }, json) 20 | end 21 | end 22 | 23 | module Response 24 | def assert_todo_not_found(response:, id:) 25 | assert_response 404 26 | 27 | expected_json = { "todo" => { "id" => id.to_s, "message" => "not found" } } 28 | 29 | assert_equal(expected_json, ::JSON.parse(response.body)) 30 | end 31 | 32 | def assert_todo_bad_request(response:) 33 | assert_response 400 34 | 35 | expected_json = { "error" => "param is missing or the value is empty: todo" } 36 | 37 | assert_equal(expected_json, ::JSON.parse(response.body)) 38 | end 39 | 40 | def assert_todo_unprocessable_entity(response:, errors:) 41 | assert_response 422 42 | 43 | assert_equal({ "todo" => errors }, ::JSON.parse(response.body)) 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /test/support/todo_list_assertions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TodoListAssertions 4 | module JSON 5 | def assert_todo_list_json(json, default: false) 6 | assert_hash_schema({ 7 | "id" => Integer, 8 | "title" => String, 9 | "default" => default, 10 | "created_at" => RegexpPatterns::ISO8601_DATETIME, 11 | "updated_at" => RegexpPatterns::ISO8601_DATETIME 12 | }, json) 13 | end 14 | end 15 | 16 | module Response 17 | def assert_todo_list_not_found(response:, id:) 18 | assert_response 404 19 | 20 | expected_json = { "todo_list" => { "id" => id.to_s, "message" => "not found" } } 21 | 22 | assert_equal(expected_json, ::JSON.parse(response.body)) 23 | end 24 | 25 | def assert_todo_list_unprocessable_entity(response:, errors:) 26 | assert_response 422 27 | 28 | assert_equal({ "todo_list" => errors }, ::JSON.parse(response.body)) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | 3 | SimpleCov.start 'rails' do 4 | enable_coverage :branch 5 | end 6 | 7 | ENV['RAILS_ENV'] ||= 'test' 8 | require_relative '../config/environment' 9 | require 'rails/test_help' 10 | 11 | Rails.root.join('test/support').glob('*.rb').each { require(_1) } 12 | 13 | class ActiveSupport::TestCase 14 | # Run tests in parallel with specified workers 15 | # parallelize(workers: :number_of_processors) 16 | 17 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 18 | fixtures :all 19 | 20 | # Add more helper methods to be used by all tests here... 21 | include HashSchemaAssertions 22 | end 23 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serradura/rails_app_to_refactor/360ca925ade0938cc52b640713744b725d4b370f/tmp/.keep -------------------------------------------------------------------------------- /vendor/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serradura/rails_app_to_refactor/360ca925ade0938cc52b640713744b725d4b370f/vendor/.keep --------------------------------------------------------------------------------