ditty 0.7.1 → 0.10.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (185) hide show
  1. checksums.yaml +4 -4
  2. data/.env.test +2 -0
  3. data/.gitignore +3 -0
  4. data/.pryrc +2 -0
  5. data/.rubocop.yml +24 -8
  6. data/.travis.yml +4 -8
  7. data/CNAME +1 -0
  8. data/Dockerfile +18 -0
  9. data/Gemfile.ci +0 -15
  10. data/Rakefile +5 -4
  11. data/Readme.md +24 -2
  12. data/_config.yml +1 -0
  13. data/config.ru +4 -4
  14. data/ditty.gemspec +31 -20
  15. data/docs/CNAME +1 -0
  16. data/docs/_config.yml +1 -0
  17. data/docs/index.md +34 -0
  18. data/exe/ditty +2 -0
  19. data/lib/ditty.rb +30 -4
  20. data/lib/ditty/cli.rb +38 -5
  21. data/lib/ditty/components/ditty.rb +82 -0
  22. data/lib/ditty/controllers/application_controller.rb +267 -0
  23. data/lib/ditty/controllers/{audit_logs.rb → audit_logs_controller.rb} +5 -7
  24. data/lib/ditty/controllers/{auth.rb → auth_controller.rb} +56 -32
  25. data/lib/ditty/controllers/{component.rb → component_controller.rb} +35 -24
  26. data/lib/ditty/controllers/{main.rb → main_controller.rb} +7 -7
  27. data/lib/ditty/controllers/roles_controller.rb +23 -0
  28. data/lib/ditty/controllers/user_login_traits_controller.rb +46 -0
  29. data/lib/ditty/controllers/{users.rb → users_controller.rb} +17 -20
  30. data/lib/ditty/db.rb +9 -5
  31. data/lib/ditty/emails/base.rb +48 -34
  32. data/lib/ditty/generators/crud_generator.rb +114 -0
  33. data/lib/ditty/generators/migration_generator.rb +26 -0
  34. data/lib/ditty/generators/project_generator.rb +52 -0
  35. data/lib/ditty/helpers/authentication.rb +6 -5
  36. data/lib/ditty/helpers/component.rb +11 -2
  37. data/lib/ditty/helpers/pundit.rb +24 -8
  38. data/lib/ditty/helpers/response.rb +38 -15
  39. data/lib/ditty/helpers/views.rb +48 -6
  40. data/lib/ditty/listener.rb +44 -14
  41. data/lib/ditty/memcached.rb +8 -0
  42. data/lib/ditty/middleware/accept_extension.rb +4 -2
  43. data/lib/ditty/middleware/error_catchall.rb +4 -2
  44. data/lib/ditty/models/audit_log.rb +1 -0
  45. data/lib/ditty/models/base.rb +13 -0
  46. data/lib/ditty/models/identity.rb +10 -7
  47. data/lib/ditty/models/role.rb +2 -0
  48. data/lib/ditty/models/user.rb +40 -3
  49. data/lib/ditty/models/user_login_trait.rb +17 -0
  50. data/lib/ditty/policies/audit_log_policy.rb +6 -6
  51. data/lib/ditty/policies/role_policy.rb +3 -3
  52. data/lib/ditty/policies/user_login_trait_policy.rb +45 -0
  53. data/lib/ditty/policies/user_policy.rb +3 -3
  54. data/lib/ditty/rubocop.rb +3 -0
  55. data/lib/ditty/seed.rb +2 -0
  56. data/lib/ditty/services/authentication.rb +31 -15
  57. data/lib/ditty/services/email.rb +22 -12
  58. data/lib/ditty/services/logger.rb +30 -13
  59. data/lib/ditty/services/pagination_wrapper.rb +9 -5
  60. data/lib/ditty/services/settings.rb +19 -7
  61. data/lib/ditty/tasks/ditty.rake +127 -0
  62. data/lib/ditty/tasks/omniauth-ldap.rake +43 -0
  63. data/lib/ditty/templates/.gitignore +5 -0
  64. data/lib/ditty/templates/.rspec +2 -0
  65. data/lib/ditty/templates/.rubocop.yml +7 -0
  66. data/lib/ditty/templates/Rakefile +12 -0
  67. data/lib/ditty/templates/application.rb +12 -0
  68. data/lib/ditty/templates/config.ru +37 -0
  69. data/lib/ditty/templates/controller.rb.erb +64 -0
  70. data/lib/ditty/templates/env.example +4 -0
  71. data/lib/ditty/templates/lib/project.rb.erb +5 -0
  72. data/lib/ditty/templates/migration.rb.erb +7 -0
  73. data/lib/ditty/templates/model.rb.erb +26 -0
  74. data/lib/ditty/templates/pids/.empty_directory +0 -0
  75. data/lib/ditty/templates/policy.rb.erb +48 -0
  76. data/{public → lib/ditty/templates/public}/browserconfig.xml +0 -0
  77. data/lib/ditty/templates/public/css/sb-admin-2.min.css +10 -0
  78. data/lib/ditty/templates/public/css/styles.css +13 -0
  79. data/lib/ditty/templates/public/favicon.ico +0 -0
  80. data/{public → lib/ditty/templates/public}/images/apple-icon.png +0 -0
  81. data/{public → lib/ditty/templates/public}/images/favicon-16x16.png +0 -0
  82. data/{public → lib/ditty/templates/public}/images/favicon-32x32.png +0 -0
  83. data/{public → lib/ditty/templates/public}/images/launcher-icon-1x.png +0 -0
  84. data/{public → lib/ditty/templates/public}/images/launcher-icon-2x.png +0 -0
  85. data/{public → lib/ditty/templates/public}/images/launcher-icon-4x.png +0 -0
  86. data/{public → lib/ditty/templates/public}/images/mstile-150x150.png +0 -0
  87. data/{public → lib/ditty/templates/public}/images/safari-pinned-tab.svg +0 -0
  88. data/lib/ditty/templates/public/js/sb-admin-2.min.js +7 -0
  89. data/lib/ditty/templates/public/js/scripts.js +1 -0
  90. data/{public/manifest.json → lib/ditty/templates/public/manifest.json.erb} +2 -2
  91. data/lib/ditty/templates/settings.yml.erb +19 -0
  92. data/lib/ditty/templates/sidekiq.rb +18 -0
  93. data/lib/ditty/templates/sidekiq.yml +9 -0
  94. data/lib/ditty/templates/spec_helper.rb +43 -0
  95. data/lib/ditty/templates/type.rb.erb +21 -0
  96. data/lib/ditty/templates/views/display.haml.tt +20 -0
  97. data/lib/ditty/templates/views/edit.haml.tt +10 -0
  98. data/lib/ditty/templates/views/form.haml.tt +11 -0
  99. data/lib/ditty/templates/views/index.haml.tt +29 -0
  100. data/lib/ditty/templates/views/new.haml.tt +10 -0
  101. data/lib/ditty/version.rb +1 -1
  102. data/lib/rubocop/cop/ditty/call_services_directly.rb +42 -0
  103. data/migrate/20181209_add_user_login_traits.rb +16 -0
  104. data/migrate/20181209_extend_audit_log.rb +12 -0
  105. data/migrate/20190220_add_parent_id_to_roles.rb +9 -0
  106. data/spec/ditty/api_spec.rb +51 -0
  107. data/spec/ditty/controllers/roles_spec.rb +67 -0
  108. data/spec/ditty/controllers/user_login_traits_spec.rb +72 -0
  109. data/spec/ditty/controllers/users_spec.rb +72 -0
  110. data/spec/ditty/emails/base_spec.rb +76 -0
  111. data/spec/ditty/emails/forgot_password_spec.rb +20 -0
  112. data/spec/ditty/helpers/component_spec.rb +85 -0
  113. data/spec/ditty/models/user_spec.rb +36 -0
  114. data/spec/ditty/services/email_spec.rb +36 -0
  115. data/spec/ditty/services/logger_spec.rb +68 -0
  116. data/spec/ditty/services/settings_spec.rb +63 -0
  117. data/spec/ditty_spec.rb +9 -0
  118. data/spec/factories.rb +46 -0
  119. data/spec/fixtures/logger.yml +17 -0
  120. data/spec/fixtures/section.yml +3 -0
  121. data/spec/fixtures/settings.yml +8 -0
  122. data/spec/spec_helper.rb +51 -0
  123. data/spec/support/api_shared_examples.rb +250 -0
  124. data/spec/support/crud_shared_examples.rb +145 -0
  125. data/views/403.haml +2 -0
  126. data/views/404.haml +2 -4
  127. data/views/500.haml +11 -0
  128. data/views/audit_logs/index.haml +32 -28
  129. data/views/auth/forgot_password.haml +32 -16
  130. data/views/auth/identity.haml +14 -13
  131. data/views/auth/ldap.haml +17 -0
  132. data/views/auth/login.haml +23 -17
  133. data/views/auth/register.haml +20 -18
  134. data/views/auth/register_identity.haml +27 -12
  135. data/views/auth/reset_password.haml +36 -19
  136. data/views/blank.haml +43 -0
  137. data/views/emails/forgot_password.haml +1 -1
  138. data/views/emails/layouts/action.haml +10 -6
  139. data/views/emails/layouts/alert.haml +2 -1
  140. data/views/emails/layouts/billing.haml +2 -1
  141. data/views/embedded.haml +17 -11
  142. data/views/error.haml +8 -3
  143. data/views/index.haml +1 -1
  144. data/views/layout.haml +45 -30
  145. data/views/partials/actions.haml +15 -14
  146. data/views/partials/content_tag.haml +0 -0
  147. data/views/partials/delete_form.haml +1 -1
  148. data/views/partials/filter_control.haml +2 -2
  149. data/views/partials/footer.haml +13 -5
  150. data/views/partials/form_control.haml +30 -19
  151. data/views/partials/form_tag.haml +1 -1
  152. data/views/partials/navitems.haml +42 -0
  153. data/views/partials/notifications.haml +12 -8
  154. data/views/partials/pager.haml +44 -25
  155. data/views/partials/search.haml +15 -11
  156. data/views/partials/sidebar.haml +15 -37
  157. data/views/partials/sort_ui.haml +2 -0
  158. data/views/partials/timespan_selector.haml +64 -0
  159. data/views/partials/topbar.haml +53 -0
  160. data/views/partials/user_associations.haml +32 -0
  161. data/views/quick_start.haml +23 -0
  162. data/views/roles/display.haml +27 -6
  163. data/views/roles/edit.haml +3 -3
  164. data/views/roles/form.haml +1 -0
  165. data/views/roles/index.haml +23 -14
  166. data/views/roles/new.haml +2 -2
  167. data/views/user_login_traits/display.haml +32 -0
  168. data/views/user_login_traits/edit.haml +10 -0
  169. data/views/user_login_traits/form.haml +5 -0
  170. data/views/user_login_traits/index.haml +28 -0
  171. data/views/user_login_traits/new.haml +10 -0
  172. data/views/users/display.haml +15 -16
  173. data/views/users/edit.haml +3 -3
  174. data/views/users/form.haml +0 -0
  175. data/views/users/index.haml +31 -24
  176. data/views/users/login_traits.haml +25 -0
  177. data/views/users/new.haml +2 -2
  178. data/views/users/profile.haml +17 -15
  179. data/views/users/user.haml +1 -1
  180. metadata +314 -76
  181. data/lib/ditty/components/app.rb +0 -77
  182. data/lib/ditty/controllers/application.rb +0 -175
  183. data/lib/ditty/controllers/roles.rb +0 -16
  184. data/lib/ditty/rake_tasks.rb +0 -102
  185. data/views/partials/navbar.haml +0 -23
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'ditty/controllers/component_controller'
5
+ require 'ditty/helpers/component'
6
+ require 'ditty/models/user'
7
+
8
+ class DummyComponentController < ::Ditty::ComponentController
9
+ set model_class: Ditty::User
10
+
11
+ FILTERS = [{ name: :email }].freeze
12
+ SEARCHABLE = %i[name email].freeze
13
+ end
14
+
15
+ describe ::Ditty::Helpers::Component do
16
+ def app
17
+ DummyComponentController
18
+ end
19
+
20
+ let(:user) { create(:super_admin_user) }
21
+ let(:model) { create(app.model_class.name.to_sym) }
22
+ let(:create_data) do
23
+ group = described_class.model_class.to_s.demodulize.underscore
24
+ identity = build(:identity).to_hash
25
+ identity['password_confirmation'] = identity['password'] = 'som3Password!'
26
+ {
27
+ group => build(described_class.model_class.name.to_sym).to_hash,
28
+ 'identity' => identity
29
+ }
30
+ end
31
+ let(:update_data) do
32
+ group = described_class.model_class.to_s.demodulize.underscore
33
+ { group => build(described_class.model_class.name.to_sym).to_hash }
34
+ end
35
+ let(:invalid_create_data) do
36
+ group = described_class.model_class.to_s.demodulize.underscore
37
+ { group => { email: 'invalidemail' } }
38
+ end
39
+ let(:invalid_update_data) do
40
+ group = described_class.model_class.to_s.demodulize.underscore
41
+ { group => { email: 'invalidemail' } }
42
+ end
43
+
44
+ before do
45
+ env 'rack.session', 'user_id' => user.id
46
+ create(:user, email: 'bruce@wayne.com')
47
+ create(:user, email: 'tony@stark.com')
48
+ end
49
+
50
+ describe 'filters' do
51
+ it 'returns the matching items' do
52
+ header 'Accept', 'application/json'
53
+ get '/', email: 'bruce@wayne.com'
54
+
55
+ response = JSON.parse last_response.body
56
+ expect(response['count']).to eq(1)
57
+ end
58
+
59
+ it 'returns no items' do
60
+ header 'Accept', 'application/json'
61
+ get '/', email: 'not found'
62
+
63
+ response = JSON.parse last_response.body
64
+ expect(response['count']).to eq(0)
65
+ end
66
+ end
67
+
68
+ describe 'search' do
69
+ it 'returns the matching items' do
70
+ header 'Accept', 'application/json'
71
+ get '/', q: 'wayne'
72
+
73
+ response = JSON.parse last_response.body
74
+ expect(response['count']).to eq(1)
75
+ end
76
+
77
+ it 'returns no items' do
78
+ header 'Accept', 'application/json'
79
+ get '/', q: 'not found'
80
+
81
+ response = JSON.parse last_response.body
82
+ expect(response['count']).to eq(0)
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'ditty/models/user'
5
+
6
+ describe ::Ditty::User, type: :model do
7
+ let(:super_admin_role) { create(:role, name: 'super_admin') }
8
+ let(:admin_role) { create(:role, name: 'admin', parent_id: super_admin_role.id) }
9
+ let!(:user_role) { create(:role, name: 'user', parent_id: admin_role.id) }
10
+ let(:super_admin) { create(:user) }
11
+ let(:user) { create(:user) }
12
+
13
+ before { super_admin.add_role(super_admin_role) }
14
+
15
+ describe '#role?(check)' do
16
+ context 'when a user has a role without a parent' do
17
+ it 'returns true only for specific role' do
18
+ expect(user).to be_role('user')
19
+ end
20
+
21
+ it 'returns false for other roles' do
22
+ %w[admin super_admin].each do |role|
23
+ expect(user).not_to be_role(role)
24
+ end
25
+ end
26
+ end
27
+
28
+ context 'when a user has a role with descendants' do
29
+ it 'returns true for all descendants' do
30
+ %w[user admin super_admin].each do |role|
31
+ expect(super_admin).to be_role(role)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'ditty/services/email'
5
+
6
+ describe ::Ditty::Services::Email do
7
+ after do
8
+ described_class.config = nil
9
+ end
10
+
11
+ context 'config!' do
12
+ it 'configures the Mail gem' do
13
+ expect(Mail).to receive(:defaults)
14
+ described_class.config!
15
+ end
16
+
17
+ it 'uses the default settings' do
18
+ expect(described_class).to receive(:default).and_call_original
19
+ described_class.config!
20
+ end
21
+ end
22
+
23
+ context 'deliver!' do
24
+ it 'autoloads a ditty email from a symbol' do
25
+ mail = Mail.new
26
+ expect(mail).to receive(:deliver!)
27
+ described_class.deliver(:base, 'test@mail.com', locals: { content: 'content' }, mail: mail)
28
+ end
29
+
30
+ it 'sends a mail object' do
31
+ mail = Mail.new
32
+ expect(mail).to receive(:deliver!)
33
+ described_class.deliver(mail)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+ require 'spec_helper'
5
+ require 'ditty/services/logger'
6
+
7
+ class TestLogger
8
+ WARN = 2
9
+ attr_accessor :level
10
+
11
+ def initialize(options = {})
12
+ @options = options
13
+ end
14
+ end
15
+
16
+ describe ::Ditty::Services::Logger, type: :service do
17
+ let(:subject) { described_class.clone }
18
+ let(:config_file) { File.read('./spec/fixtures/logger.yml') }
19
+
20
+ context 'initialize' do
21
+ it '.instance always refers to the same instance' do
22
+ expect(subject.instance).to eq subject.instance
23
+ end
24
+
25
+ it "creates default logger if config file does't exist" do
26
+ expect(subject.instance.loggers[0]).to be_instance_of Logger
27
+ end
28
+
29
+ it 'reads config from file and creates an array of loggers' do
30
+ ::Ditty::Services::Settings.values = nil
31
+ allow(File).to receive(:file?).and_return(false)
32
+ allow(File).to receive(:file?).with('./config/logger.yml').and_return(true)
33
+ allow(File).to receive(:read).and_return(config_file)
34
+
35
+ expect(subject.instance.loggers.size).to eq 4
36
+ expect(subject.instance.loggers[0]).to be_instance_of Logger
37
+ expect(subject.instance.loggers[1]).to be_instance_of TestLogger
38
+ end
39
+
40
+ it 'sets the correct logging level' do
41
+ ::Ditty::Services::Settings.values = nil
42
+ allow(File).to receive(:file?).and_return(false)
43
+ allow(File).to receive(:file?).with('./config/logger.yml').and_return(true)
44
+ allow(File).to receive(:read).and_return(config_file)
45
+ expect(subject.instance.loggers[0].level).to eq Logger::DEBUG
46
+ expect(subject.instance.loggers[2].level).to eq Logger::INFO
47
+ expect(subject.instance.loggers[3].level).to eq Logger::WARN
48
+ end
49
+ end
50
+
51
+ context 'send messages' do
52
+ it 'receives message and passes it to the loggers' do
53
+ ::Ditty::Services::Settings.values = nil
54
+ allow(File).to receive(:file?).and_return(false)
55
+ allow(File).to receive(:file?).with('./config/logger.yml').and_return(true)
56
+ allow(File).to receive(:read).and_return(config_file)
57
+ allow(Logger).to receive(:warn).with('Some message')
58
+ allow(TestLogger).to receive(:warn).with('Some message')
59
+
60
+ expect(subject.instance.loggers[0]).to receive(:warn).with('Some message')
61
+ expect(subject.instance.loggers[1]).to receive(:warn).with('Some message')
62
+ expect($stdout).to receive(:write).with(/Some message$/)
63
+ expect($stderr).to receive(:write).with(/Some message$/)
64
+
65
+ subject.instance.warn 'Some message'
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'ditty/services/settings'
5
+
6
+ describe ::Ditty::Services::Settings do
7
+ def setup_files
8
+ settings = File.read('./spec/fixtures/settings.yml')
9
+ section = File.read('./spec/fixtures/section.yml')
10
+
11
+ allow(File).to receive(:file?).and_return(false)
12
+ allow(File).to receive(:file?).with('./config/settings.yml').and_return(true)
13
+ allow(File).to receive(:file?).with('./config/section.yml').and_return(true)
14
+
15
+ allow(File).to receive(:read).with('./config/settings.yml').and_return(settings)
16
+ allow(File).to receive(:read).with('./config/section.yml').and_return(section)
17
+ end
18
+
19
+ describe '#[]' do
20
+ before do
21
+ setup_files
22
+ described_class.values = nil
23
+ end
24
+
25
+ it 'returns the specified values from the global settings' do
26
+ expect(described_class[:option_a]).to eq 1
27
+ end
28
+
29
+ it 'allows access to sectional settings' do
30
+ expect(described_class[:no_file_section]).to include(section_1: 2, section_2: 'set')
31
+ end
32
+
33
+ it 'allows using dots to travers' do
34
+ expect(described_class['nested.option']).to eq 'value'
35
+ end
36
+ end
37
+
38
+ describe '#values' do
39
+ context 'uses the global file' do
40
+ before do
41
+ setup_files
42
+ end
43
+
44
+ it 'to return global settings' do
45
+ expect(described_class.values).to include(option_a: 1, option_b: 'value')
46
+ end
47
+
48
+ it 'to return sectional settings' do
49
+ expect(described_class.values(:no_file_section)).to include(section_1: 2, section_2: 'set')
50
+ end
51
+ end
52
+
53
+ context 'uses the sectional file' do
54
+ before do
55
+ setup_files
56
+ end
57
+
58
+ it 'prefers the sectional settings file' do
59
+ expect(described_class.values(:section)).to include(section_1: 3, section_2: 'section')
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe ::Ditty do
6
+ it 'has a version number' do
7
+ expect(Ditty::VERSION).not_to be nil
8
+ end
9
+ end
data/spec/factories.rb ADDED
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faker'
4
+ require 'ditty/models/user'
5
+ require 'ditty/models/identity'
6
+ require 'ditty/models/role'
7
+ require 'ditty/models/user_login_trait'
8
+
9
+ FactoryBot.define do
10
+ to_create(&:save)
11
+
12
+ sequence(:email) { |n| "person-#{n}@example.com" }
13
+ sequence(:name) { |n| "Name-#{n}" }
14
+
15
+ factory :user, class: 'Ditty::User', aliases: [:'Ditty::User'] do
16
+ email
17
+
18
+ after(:create) do |user, _evaluator|
19
+ create(:identity, user: user)
20
+ end
21
+
22
+ factory :super_admin_user do
23
+ after(:create) do |user, _evaluator|
24
+ user.add_role(Ditty::Role.find_or_create(name: 'super_admin'))
25
+ end
26
+ end
27
+ end
28
+
29
+ factory :identity, class: 'Ditty::Identity', aliases: [:'Ditty::Identity'] do
30
+ username { generate :email }
31
+ crypted_password { 'som3Password!' }
32
+ end
33
+
34
+ factory :role, class: 'Ditty::Role', aliases: [:'Ditty::Role'] do
35
+ name { "Role #{generate(:name)}" }
36
+ parent_id { nil }
37
+ end
38
+
39
+ factory :user_login_trait, class: 'Ditty::UserLoginTrait', aliases: [:'Ditty::UserLoginTrait'] do
40
+ association :user, strategy: :create, factory: :user
41
+ ip_address { Faker::Internet.ip_v4_address }
42
+ platform { Faker::Device.platform }
43
+ device { Faker::Device.model_name }
44
+ browser { 'Firefox' }
45
+ end
46
+ end
@@ -0,0 +1,17 @@
1
+ loggers:
2
+ - name: file
3
+ class: Logger
4
+ level: 'DEBUG'
5
+ - name: ES
6
+ class: TestLogger
7
+ options:
8
+ url: 'http://logging.ditty.io:9200'
9
+ log: false
10
+ - name: stdout
11
+ class: Logger
12
+ options: '$stdout'
13
+ level: INFO
14
+ - name: stderr
15
+ class: Logger
16
+ options: '$stderr'
17
+ level: WARN
@@ -0,0 +1,3 @@
1
+ ---
2
+ section_1: 3
3
+ section_2: section
@@ -0,0 +1,8 @@
1
+ ---
2
+ option_a: 1
3
+ option_b: value
4
+ no_file_section:
5
+ section_1: 2
6
+ section_2: set
7
+ nested:
8
+ option: value
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dotenv'
4
+ Dotenv.load('.env.test')
5
+ Dotenv.overload('.env.test.local')
6
+
7
+ ENV['APP_ENV'] ||= 'test'
8
+ ENV['RACK_ENV'] ||= 'test'
9
+ require 'simplecov'
10
+ SimpleCov.start
11
+
12
+ ENV['DATABASE_URL'] ||= 'sqlite::memory:'
13
+
14
+ require 'ditty'
15
+ require 'ditty/db'
16
+ require 'rspec'
17
+ require 'rack/test'
18
+ require 'factory_bot'
19
+ require 'database_cleaner'
20
+ require 'timecop'
21
+
22
+ if ENV['DATABASE_URL'] == 'sqlite::memory:'
23
+ folder = File.expand_path("#{File.dirname(__FILE__)}/../migrate")
24
+ Sequel.extension :migration
25
+ Sequel::Migrator.apply(DB, folder)
26
+
27
+ # Seed the DB
28
+ require 'ditty/seed'
29
+ end
30
+
31
+ Ditty.component :ditty
32
+ RSpec.configure do |config|
33
+ config.include Rack::Test::Methods
34
+ config.include FactoryBot::Syntax::Methods
35
+
36
+ config.alias_example_to :fit, focus: true
37
+ config.filter_run focus: true
38
+ config.run_all_when_everything_filtered = true
39
+
40
+ config.before(:suite) do
41
+ DatabaseCleaner.strategy = :transaction
42
+ FactoryBot.find_definitions
43
+ Timecop.freeze
44
+ end
45
+
46
+ config.around do |example|
47
+ DatabaseCleaner.cleaning do
48
+ example.run
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,250 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'active_support/core_ext/hash/except'
5
+
6
+ shared_examples 'an API interface' do |subject, params|
7
+ before { create(subject) }
8
+
9
+ context 'GET /' do
10
+ it 'returns HTML when requested' do
11
+ header 'Accept', 'text/html'
12
+ get '/'
13
+
14
+ expect(last_response).to be_ok
15
+ expect(last_response.headers['Content-Type']).to include('text/html;charset=utf-8')
16
+ end
17
+
18
+ it 'returns JSON when requested' do
19
+ header 'Accept', 'application/json'
20
+ get '/'
21
+
22
+ expect(last_response).to be_ok
23
+ expect(last_response.headers).to include('Content-Type' => 'application/json')
24
+ expect { JSON.parse(last_response.body) }.not_to raise_error
25
+ end
26
+
27
+ it 'returns a list object' do
28
+ header 'Accept', 'application/json'
29
+ get '/'
30
+
31
+ response = JSON.parse last_response.body
32
+ expect(response).to include('page', 'count', 'total', 'items')
33
+ expect(response['page']).to be_an Integer
34
+ expect(response['count']).to be_an Integer
35
+ expect(response['total']).to be_an Integer
36
+ expect(response['items']).to be_an Array
37
+ end
38
+ end
39
+
40
+ context 'GET /id' do
41
+ let(:entity) { create(subject) }
42
+
43
+ it 'returns HTML when requested' do
44
+ header 'Accept', 'text/html'
45
+ get "/#{entity.id}"
46
+
47
+ expect(last_response).to be_ok
48
+ expect(last_response.headers).to include('Content-Type' => 'text/html;charset=utf-8')
49
+ end
50
+
51
+ it 'returns JSON when requested' do
52
+ header 'Accept', 'application/json'
53
+ get "/#{entity.id}"
54
+
55
+ expect(last_response).to be_ok
56
+ expect(last_response.headers).to include('Content-Type' => 'application/json')
57
+ expect { JSON.parse(last_response.body) }.not_to raise_error
58
+ end
59
+
60
+ it 'returns the fetched object' do
61
+ header 'Accept', 'application/json'
62
+ get "/#{entity.id}"
63
+
64
+ response = JSON.parse last_response.body
65
+ expect(response).to be_a Hash
66
+ entity_to_json = JSON.parse entity.values.to_json
67
+ expect(response).to include(entity_to_json)
68
+ end
69
+ end
70
+
71
+ context 'POST /' do
72
+ it 'returns HTML when requested' do
73
+ header 'Accept', 'text/html'
74
+ header 'Content-Type', 'application/x-www-form-urlencoded'
75
+ params[subject] = build(subject).to_hash
76
+ post '/', params
77
+
78
+ expect(last_response.headers).to include('Content-Type' => 'text/html;charset=utf-8')
79
+ end
80
+
81
+ it 'returns a 302 Redirect response for a HTML Request' do
82
+ header 'Accept', 'text/html'
83
+ header 'Content-Type', 'application/x-www-form-urlencoded'
84
+ params[subject] = build(subject).to_hash
85
+ post '/', params
86
+
87
+ expect(last_response.status).to eq 302
88
+ expect(last_response.headers).to include('Location')
89
+ end
90
+
91
+ it 'returns JSON when requested' do
92
+ header 'Accept', 'application/json'
93
+ header 'Content-Type', 'application/json'
94
+ params[subject] = build(subject).to_hash
95
+ post '/', params.to_json
96
+
97
+ expect(last_response.headers).to include('Content-Type' => 'application/json')
98
+ end
99
+
100
+ it 'returns a 201 Created response for a JSON Request' do
101
+ header 'Accept', 'application/json'
102
+ header 'Content-Type', 'application/json'
103
+ params[subject] = build(subject).to_hash
104
+ post '/', params.to_json
105
+
106
+ expect(last_response.status).to eq 201
107
+ end
108
+
109
+ it 'returns a Location Header for a JSON Request' do
110
+ header 'Accept', 'application/json'
111
+ header 'Content-Type', 'application/json'
112
+ params[subject] = build(subject).to_hash
113
+ post '/', params.to_json
114
+
115
+ expect(last_response.headers).to include 'Location'
116
+ end
117
+
118
+ it 'returns an empty body for a JSON Request' do
119
+ header 'Accept', 'application/json'
120
+ header 'Content-Type', 'application/json'
121
+ params[subject] = build(subject).to_hash
122
+ post '/', params.to_json
123
+
124
+ expect(last_response.body).to eq ''
125
+ end
126
+ end
127
+
128
+ context 'PUT /:id' do
129
+ let(:entity) { create(subject) }
130
+
131
+ it 'returns HTML when requested' do
132
+ header 'Accept', 'text/html'
133
+ header 'Content-Type', 'application/x-www-form-urlencoded'
134
+
135
+ values = entity.to_hash.except(:id)
136
+ params[subject] = values
137
+ put "/#{entity.id}", params
138
+
139
+ expect(last_response.headers).to include('Content-Type' => 'text/html;charset=utf-8')
140
+ end
141
+
142
+ it 'returns a 302 Redirect response for a HTML Request' do
143
+ header 'Accept', 'text/html'
144
+ header 'Content-Type', 'application/x-www-form-urlencoded'
145
+
146
+ values = entity.to_hash.except(:id)
147
+ params[subject] = values
148
+ put "/#{entity.id}", params
149
+
150
+ expect(last_response.status).to eq 302
151
+ expect(last_response.headers).to include('Location')
152
+ end
153
+
154
+ it 'returns JSON when requested' do
155
+ header 'Accept', 'application/json'
156
+ header 'Content-Type', 'application/json'
157
+
158
+ values = entity.to_hash.except(:id)
159
+ params[subject] = values
160
+ put "/#{entity.id}", params.to_json
161
+
162
+ expect(last_response.headers).to include('Content-Type' => 'application/json')
163
+ end
164
+
165
+ it 'returns a 200 OK response for a JSON Request' do
166
+ header 'Accept', 'application/json'
167
+ header 'Content-Type', 'application/json'
168
+
169
+ values = entity.to_hash.except(:id)
170
+ params[subject] = values
171
+ put "/#{entity.id}", params.to_json
172
+
173
+ expect(last_response.status).to eq 200
174
+ end
175
+
176
+ it 'returns a Location Header for a JSON Request' do
177
+ header 'Accept', 'application/json'
178
+ header 'Content-Type', 'application/json'
179
+
180
+ values = entity.to_hash.except(:id)
181
+ params[subject] = values
182
+ put "/#{entity.id}", params.to_json
183
+
184
+ expect(last_response.headers).to include 'Location'
185
+ end
186
+
187
+ it 'returns the updated entity in the body for a JSON Request' do
188
+ header 'Accept', 'application/json'
189
+ header 'Content-Type', 'application/json'
190
+
191
+ values = entity.to_hash.except(:id)
192
+ params[subject] = values
193
+ put "/#{entity.id}", params.to_json
194
+
195
+ response = JSON.parse last_response.body
196
+ entity_to_hash = JSON.parse entity.values.to_json
197
+ expect(response).to eq entity_to_hash
198
+ end
199
+ end
200
+
201
+ context 'DELETE /:id' do
202
+ let(:entity) { create(subject) }
203
+
204
+ it 'returns HTML when requested' do
205
+ header 'Accept', 'text/html'
206
+ header 'Content-Type', 'application/x-www-form-urlencoded'
207
+
208
+ delete "/#{entity.id}"
209
+
210
+ expect(last_response.headers).to include('Content-Type' => 'text/html;charset=utf-8')
211
+ end
212
+
213
+ it 'returns a 302 Redirect response for a HTML Request' do
214
+ header 'Accept', 'text/html'
215
+ header 'Content-Type', 'application/x-www-form-urlencoded'
216
+
217
+ delete "/#{entity.id}"
218
+
219
+ expect(last_response.status).to eq 302
220
+ expect(last_response.headers).to include('Location')
221
+ end
222
+
223
+ it 'returns JSON when requested' do
224
+ header 'Accept', 'application/json'
225
+ header 'Content-Type', 'application/json'
226
+
227
+ delete "/#{entity.id}"
228
+
229
+ expect(last_response.headers).to include('X-Content-Type-Options' => 'nosniff')
230
+ end
231
+
232
+ it 'returns a 204 No Content response for a JSON Request' do
233
+ header 'Accept', 'application/json'
234
+ header 'Content-Type', 'application/json'
235
+
236
+ delete "/#{entity.id}"
237
+
238
+ expect(last_response.status).to eq 204
239
+ end
240
+
241
+ it 'returns an empty body for a JSON Request' do
242
+ header 'Accept', 'application/json'
243
+ header 'Content-Type', 'application/json'
244
+
245
+ delete "/#{entity.id}"
246
+
247
+ expect(last_response.body).to eq ''
248
+ end
249
+ end
250
+ end