ditty 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 35f3891fe07db1d74d92de59cbcf9ebde20c7aa9f5fdc995a5bed98445ac38de
4
- data.tar.gz: 2ed19a9d156c4a043aa3d29b8ad070ed70da16637a88577ef74630734038868e
3
+ metadata.gz: a7260a6694f316ec8f4e3388932e7a749030f7fb6fc88657012f15a863069639
4
+ data.tar.gz: 3f0e6db669807bc153111ceaa182cbbc5323f90f0acf535e3bdb6bd199b531cc
5
5
  SHA512:
6
- metadata.gz: b11cb4cdab8e3afa910750884a5129becfe68202f779a35011e368a3d872724a41d2b3bd90b06485fdbdea740fbbe1c7165a8da50095ab7c7d767c28c2595185
7
- data.tar.gz: 9e06c150bc6fd18dcc564dae5cf43bc3d9a5327541df1d64dd7b6f305381d83102ef1c9105ae4a9bcfe82cfa11881eda1fb93ec2321bd549072d92c2ee0c4acb
6
+ metadata.gz: fd5b41ce540f365575aeb1c7c33ffc487f87585894a0d1267428dc5bba8a60ea9b041fa9f0c8e8bcda62b265f94c989b3938c82e76a1770269a0abc8a26ed481
7
+ data.tar.gz: 385f6cae60979bc716d3268a07856255fc19beaa9652087c1d9f748042074c927d4fcb259188667ef6724083feb76004871c5e69e76d5c8eae4414dd63781997
@@ -15,7 +15,7 @@ Gem::Specification.new do |spec|
15
15
  spec.homepage = 'https://github.com/eagerelk/ditty'
16
16
  spec.license = 'MIT'
17
17
 
18
- spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.files = `git ls-files -z`.split("\x0") #.reject { |f| f.match(%r{^(test|spec|features)/}) }
19
19
  spec.bindir = 'exe'
20
20
  spec.executables = ['ditty']
21
21
  spec.require_paths = ['lib']
@@ -59,7 +59,7 @@ module Ditty
59
59
  puts 'Ditty DB Schema Dumped'
60
60
  end
61
61
 
62
- desc 'seed', 'Seed the predefined seeind data'
62
+ desc 'seed', 'Seed the predefined seeding data'
63
63
  def seed
64
64
  Rake::Task['ditty:seed'].invoke
65
65
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ditty
4
- VERSION = '0.9.0'
4
+ VERSION = '0.9.1'
5
5
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ Dir.glob('./lib/ditty/controllers/*.rb').each { |f| require f }
5
+ require 'support/api_shared_examples'
6
+
7
+ describe ::Ditty::RolesController, type: :controller do
8
+ def app
9
+ described_class
10
+ end
11
+
12
+ let(:user) { create(:super_admin_user) }
13
+
14
+ before do
15
+ env 'rack.session', 'user_id' => user.id
16
+ end
17
+
18
+ it_behaves_like 'an API interface', :role, {}
19
+ end
20
+
21
+ describe ::Ditty::UsersController, type: :controller do
22
+ def app
23
+ described_class
24
+ end
25
+
26
+ let(:user) { create(:super_admin_user) }
27
+
28
+ before { env 'rack.session', 'user_id' => user.id }
29
+
30
+ params = {
31
+ identity: {
32
+ username: 'test-user@abc.abc',
33
+ password: 'som3Password!',
34
+ password_confirmation: 'som3Password!'
35
+ }
36
+ }
37
+
38
+ it_behaves_like 'an API interface', :user, params
39
+ end
40
+
41
+ describe ::Ditty::UserLoginTraitsController, type: :controller do
42
+ def app
43
+ described_class
44
+ end
45
+
46
+ let(:user) { create(:super_admin_user) }
47
+
48
+ before { env 'rack.session', 'user_id' => user.id }
49
+
50
+ it_behaves_like 'an API interface', :user_login_trait, {}
51
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'ditty/controllers/roles_controller'
5
+ require 'support/crud_shared_examples'
6
+
7
+ describe ::Ditty::RolesController do
8
+ def app
9
+ described_class
10
+ end
11
+
12
+ context 'as super_admin_user' do
13
+ let(:user) { create(:super_admin_user) }
14
+ let(:model) { create(app.model_class.name.to_sym) }
15
+ let(:create_data) do
16
+ group = described_class.model_class.to_s.demodulize.underscore
17
+ { group => build(described_class.model_class.name.to_sym).to_hash }
18
+ end
19
+ let(:update_data) do
20
+ group = described_class.model_class.to_s.demodulize.underscore
21
+ { group => build(described_class.model_class.name.to_sym).to_hash }
22
+ end
23
+ let(:invalid_create_data) do
24
+ group = described_class.model_class.to_s.demodulize.underscore
25
+ { group => { name: '' } }
26
+ end
27
+ let(:invalid_update_data) do
28
+ group = described_class.model_class.to_s.demodulize.underscore
29
+ { group => { name: '' } }
30
+ end
31
+
32
+ before do
33
+ # Log in
34
+ env 'rack.session', 'user_id' => user.id
35
+ end
36
+
37
+ it_behaves_like 'a CRUD Controller', '/roles'
38
+ end
39
+
40
+ context 'as user' do
41
+ let(:user) { create(:user) }
42
+ let(:model) { create(app.model_class.name.to_sym) }
43
+ let(:create_data) do
44
+ group = described_class.model_class.to_s.demodulize.underscore
45
+ { group => build(described_class.model_class.name.to_sym).to_hash }
46
+ end
47
+ let(:update_data) do
48
+ group = described_class.model_class.to_s.demodulize.underscore
49
+ { group => build(described_class.model_class.name.to_sym).to_hash }
50
+ end
51
+ let(:invalid_create_data) do
52
+ group = described_class.model_class.to_s.demodulize.underscore
53
+ { group => { name: '' } }
54
+ end
55
+ let(:invalid_update_data) do
56
+ group = described_class.model_class.to_s.demodulize.underscore
57
+ { group => { name: '' } }
58
+ end
59
+
60
+ before do
61
+ # Log in
62
+ env 'rack.session', 'user_id' => user.id
63
+ end
64
+
65
+ it_behaves_like 'a CRUD Controller', '/roles'
66
+ end
67
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'ditty/controllers/user_login_traits_controller'
5
+ require 'support/crud_shared_examples'
6
+
7
+ describe ::Ditty::UserLoginTraitsController do
8
+ def app
9
+ described_class
10
+ end
11
+
12
+ context 'as super_admin_user' do
13
+ let(:user) { create(:super_admin_user) }
14
+ let(:model) { create(app.model_class.name.to_sym) }
15
+ let(:create_data) do
16
+ group = described_class.model_class.to_s.demodulize.underscore
17
+ identity = build(:identity).to_hash
18
+ identity['password_confirmation'] = identity['password'] = 'som3Password!'
19
+ {
20
+ group => build(described_class.model_class.name.to_sym).to_hash,
21
+ 'identity' => identity
22
+ }
23
+ end
24
+ let(:update_data) do
25
+ group = described_class.model_class.to_s.demodulize.underscore
26
+ { group => build(described_class.model_class.name.to_sym).to_hash }
27
+ end
28
+ let(:invalid_create_data) do
29
+ group = described_class.model_class.to_s.demodulize.underscore
30
+ { group => { user_id: nil } }
31
+ end
32
+ let(:invalid_update_data) do
33
+ group = described_class.model_class.to_s.demodulize.underscore
34
+ { group => { user_id: nil } }
35
+ end
36
+
37
+ before do
38
+ # Log in
39
+ env 'rack.session', 'user_id' => user.id
40
+ end
41
+
42
+ it_behaves_like 'a CRUD Controller', '/user-login-traits'
43
+ end
44
+
45
+ context 'as user' do
46
+ let(:user) { create(:user) }
47
+ let(:model) { create(app.model_class.name.to_sym, user: user) }
48
+ let(:create_data) do
49
+ group = described_class.model_class.to_s.demodulize.underscore
50
+ { group => build(described_class.model_class.name.to_sym).to_hash }
51
+ end
52
+ let(:update_data) do
53
+ group = described_class.model_class.to_s.demodulize.underscore
54
+ { group => build(described_class.model_class.name.to_sym).to_hash }
55
+ end
56
+ let(:invalid_create_data) do
57
+ group = described_class.model_class.to_s.demodulize.underscore
58
+ { group => { user_id: nil } }
59
+ end
60
+ let(:invalid_update_data) do
61
+ group = described_class.model_class.to_s.demodulize.underscore
62
+ { group => { user_id: nil } }
63
+ end
64
+
65
+ before do
66
+ # Log in
67
+ env 'rack.session', 'user_id' => user.id
68
+ end
69
+
70
+ it_behaves_like 'a CRUD Controller', '/user-login-traits'
71
+ end
72
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'ditty/controllers/users_controller'
5
+ require 'support/crud_shared_examples'
6
+
7
+ describe ::Ditty::UsersController do
8
+ def app
9
+ described_class
10
+ end
11
+
12
+ context 'as super_admin_user' do
13
+ let(:user) { create(:super_admin_user) }
14
+ let(:model) { create(app.model_class.name.to_sym) }
15
+ let(:create_data) do
16
+ group = described_class.model_class.to_s.demodulize.underscore
17
+ identity = build(:identity).to_hash
18
+ identity['password_confirmation'] = identity['password'] = 'som3Password!'
19
+ {
20
+ group => build(described_class.model_class.name.to_sym).to_hash,
21
+ 'identity' => identity
22
+ }
23
+ end
24
+ let(:update_data) do
25
+ group = described_class.model_class.to_s.demodulize.underscore
26
+ { group => build(described_class.model_class.name.to_sym).to_hash }
27
+ end
28
+ let(:invalid_create_data) do
29
+ group = described_class.model_class.to_s.demodulize.underscore
30
+ { group => { email: 'invalidemail' } }
31
+ end
32
+ let(:invalid_update_data) do
33
+ group = described_class.model_class.to_s.demodulize.underscore
34
+ { group => { email: 'invalidemail' } }
35
+ end
36
+
37
+ before do
38
+ # Log in
39
+ env 'rack.session', 'user_id' => user.id
40
+ end
41
+
42
+ it_behaves_like 'a CRUD Controller', '/users'
43
+ end
44
+
45
+ context 'as user' do
46
+ let(:user) { create(:user) }
47
+ let(:model) { create(app.model_class.name.to_sym) }
48
+ let(:create_data) do
49
+ group = described_class.model_class.to_s.demodulize.underscore
50
+ { group => build(described_class.model_class.name.to_sym).to_hash }
51
+ end
52
+ let(:update_data) do
53
+ group = described_class.model_class.to_s.demodulize.underscore
54
+ { group => build(described_class.model_class.name.to_sym).to_hash }
55
+ end
56
+ let(:invalid_create_data) do
57
+ group = described_class.model_class.to_s.demodulize.underscore
58
+ { group => { email: 'invalidemail' } }
59
+ end
60
+ let(:invalid_update_data) do
61
+ group = described_class.model_class.to_s.demodulize.underscore
62
+ { group => { email: 'invalidemail' } }
63
+ end
64
+
65
+ before do
66
+ # Log in
67
+ env 'rack.session', 'user_id' => user.id
68
+ end
69
+
70
+ it_behaves_like 'a CRUD Controller', '/users'
71
+ end
72
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'ditty/emails/base'
5
+ require 'mail'
6
+
7
+ describe ::Ditty::Emails::Base do
8
+ let(:mail) do
9
+ mail = Mail.new
10
+ allow(mail).to receive(:deliver!)
11
+ mail
12
+ end
13
+
14
+ context '.new' do
15
+ it 'defaults to base options' do
16
+ expect(subject.options).to include subject: '(No Subject)', from: 'no-reply@ditty.io', view: :base
17
+ end
18
+
19
+ it 'allows the use of layouts' do
20
+ skip 'Test is not accurate. The class no longer uses body.'
21
+ base = described_class.new(layout: 'action', mail: mail)
22
+ expect(mail).to receive(:body).with(/^<!DOCTYPE html>/m)
23
+ base.deliver!('test@email.com')
24
+ end
25
+ end
26
+
27
+ context '.deliver!' do
28
+ it 'delivers the email to the specified email address' do
29
+ expect(mail).to receive(:to).with('test@email.com')
30
+ expect(mail).to receive(:deliver!)
31
+ described_class.deliver!('test@email.com', mail: mail)
32
+ end
33
+
34
+ it 'passes down local variables' do
35
+ skip 'Test is not accurate. The class no longer uses body.'
36
+ expect(mail).to receive(:body).with("test content\n")
37
+ described_class.deliver!('test@email.com', locals: { content: 'test content' }, mail: mail)
38
+ end
39
+
40
+ it 'sets the email\'s subject and from address' do
41
+ expect(mail).to receive(:subject).with('test subject')
42
+ expect(mail).to receive(:from).with('from@test.com')
43
+ described_class.deliver!('test@email.com', subject: 'test subject', from: 'from@test.com', mail: mail)
44
+ end
45
+ end
46
+
47
+ context '#deliver!' do
48
+ it 'delivers the email to the specified email address' do
49
+ expect(mail).to receive(:to).with('test2@email.com')
50
+ base = described_class.new(mail: mail)
51
+ base.deliver!('test2@email.com')
52
+ end
53
+
54
+ it 'passes the local variables to the template' do
55
+ skip 'Test is not accurate. The class no longer uses body.'
56
+ expect(mail).to receive(:body).with("test content\n")
57
+ base = described_class.new(mail: mail)
58
+ base.deliver!('test@email.com', content: 'test content')
59
+ end
60
+
61
+ it 'sets the email\'s subject and from address' do
62
+ expect(mail).to receive(:subject).with('test subject')
63
+ expect(mail).to receive(:from).with('from@test.com')
64
+ base = described_class.new(subject: 'test subject', from: 'from@test.com', mail: mail)
65
+ base.deliver!('test@email.com')
66
+ end
67
+ end
68
+
69
+ context 'method_missing' do
70
+ it 'passes unknown message to the underlying mail object' do
71
+ expect(mail).to receive(:cc).with('cc@test.com')
72
+ base = described_class.new(mail: mail)
73
+ base.cc 'cc@test.com'
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'ditty/emails/forgot_password'
5
+ require 'mail'
6
+
7
+ describe ::Ditty::Emails::ForgotPassword do
8
+ let(:mail) do
9
+ mail = Mail.new
10
+ allow(mail).to receive(:deliver!)
11
+ mail
12
+ end
13
+
14
+ context '.new' do
15
+ it 'defaults to base options' do
16
+ expect(subject.options).to include subject: 'Request to reset password', from: 'no-reply@ditty.io', view: :forgot_password
17
+ end
18
+ end
19
+ end
@@ -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.role?('user')).to be_truthy
19
+ end
20
+
21
+ it 'returns false for other roles' do
22
+ %w[admin super_admin].each do |role|
23
+ expect(user.role?(role)).to be_falsy
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.role?(role)).to be_truthy
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,67 @@
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
+ def initialize(options = {})
11
+ @options = options
12
+ end
13
+ end
14
+
15
+ describe ::Ditty::Services::Logger, type: :service do
16
+ let(:subject) { described_class.clone }
17
+ let(:config_file) { File.read('./spec/fixtures/logger.yml') }
18
+
19
+ context 'initialize' do
20
+ it '.instance always refers to the same instance' do
21
+ expect(subject.instance).to eq subject.instance
22
+ end
23
+
24
+ it "creates default logger if config file does't exist" do
25
+ expect(subject.instance.loggers[0]).to be_instance_of Logger
26
+ end
27
+
28
+ it 'reads config from file and creates an array of loggers' do
29
+ ::Ditty::Services::Settings.values = nil
30
+ allow(File).to receive(:'file?').and_return(false)
31
+ allow(File).to receive(:'file?').with('./config/logger.yml').and_return(true)
32
+ allow(File).to receive(:read).and_return(config_file)
33
+
34
+ expect(subject.instance.loggers.size).to eq 4
35
+ expect(subject.instance.loggers[0]).to be_instance_of Logger
36
+ expect(subject.instance.loggers[1]).to be_instance_of TestLogger
37
+ end
38
+
39
+ it 'sets the correct logging level' do
40
+ ::Ditty::Services::Settings.values = nil
41
+ allow(File).to receive(:'file?').and_return(false)
42
+ allow(File).to receive(:'file?').with('./config/logger.yml').and_return(true)
43
+ allow(File).to receive(:read).and_return(config_file)
44
+ expect(subject.instance.loggers[0].level).to eq Logger::DEBUG
45
+ expect(subject.instance.loggers[2].level).to eq Logger::INFO
46
+ expect(subject.instance.loggers[3].level).to eq Logger::WARN
47
+ end
48
+ end
49
+
50
+ context 'send messages' do
51
+ it 'receives message and passes it to the loggers' do
52
+ ::Ditty::Services::Settings.values = nil
53
+ allow(File).to receive(:'file?').and_return(false)
54
+ allow(File).to receive(:'file?').with('./config/logger.yml').and_return(true)
55
+ allow(File).to receive(:read).and_return(config_file)
56
+ allow(Logger).to receive(:warn).with('Some message')
57
+ allow(TestLogger).to receive(:warn).with('Some message')
58
+
59
+ expect(subject.instance.loggers[0]).to receive(:warn).with('Some message')
60
+ expect(subject.instance.loggers[1]).to receive(:warn).with('Some message')
61
+ expect($stdout).to receive(:write).with(/Some message$/)
62
+ expect($stderr).to receive(:write).with(/Some message$/)
63
+
64
+ subject.instance.warn 'Some message'
65
+ end
66
+ end
67
+ 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
+ context '#[]' 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
+ context '#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
@@ -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,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ ENV['APP_ENV'] ||= 'test'
4
+ ENV['RACK_ENV'] ||= 'test'
5
+ require 'simplecov'
6
+ SimpleCov.start
7
+
8
+ ENV['DATABASE_URL'] ||= 'sqlite::memory:'
9
+
10
+ require 'ditty'
11
+ require 'ditty/db'
12
+ require 'rspec'
13
+ require 'rack/test'
14
+ require 'factory_bot'
15
+ require 'database_cleaner'
16
+ require 'timecop'
17
+
18
+ if ENV['DATABASE_URL'] == 'sqlite::memory:'
19
+ folder = File.expand_path(File.dirname(__FILE__) + '/../migrate')
20
+ Sequel.extension :migration
21
+ Sequel::Migrator.apply(DB, folder)
22
+
23
+ # Seed the DB
24
+ require 'ditty/seed'
25
+ end
26
+
27
+ Ditty.component :ditty
28
+ RSpec.configure do |config|
29
+ config.include Rack::Test::Methods
30
+ config.include FactoryBot::Syntax::Methods
31
+
32
+ config.alias_example_to :fit, focus: true
33
+ config.filter_run focus: true
34
+ config.run_all_when_everything_filtered = true
35
+
36
+ config.before(:suite) do
37
+ DatabaseCleaner.strategy = :transaction
38
+ FactoryBot.find_definitions
39
+ Timecop.freeze
40
+ end
41
+
42
+ config.around do |example|
43
+ DatabaseCleaner.cleaning do
44
+ example.run
45
+ end
46
+ end
47
+ 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
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ shared_examples 'a CRUD Controller' do |route|
4
+ context 'GET' do
5
+ it '/doesnotexist' do
6
+ get '/doesnotexist'
7
+ expect(last_response).not_to be_ok
8
+ expect(last_response.status).to eq 404
9
+ end
10
+
11
+ it route.to_s do
12
+ model # Ensure that there's at least one item in the list
13
+ get '/'
14
+
15
+ if Pundit.policy(user, app.model_class).list?
16
+ expect(last_response).to be_ok, "Expected OK response, got #{last_response.status}"
17
+ else
18
+ expect(last_response).not_to be_ok, "Expected a NOT OK response, got #{last_response.status}"
19
+ end
20
+ end
21
+
22
+ it "#{route}?count=1&page=1" do
23
+ model # Ensure that there's at least one item in the list
24
+ get '/?count=1&page=1'
25
+
26
+ if Pundit.policy(user, app.model_class).list?
27
+ expect(last_response).to be_ok, "Expected OK response, got #{last_response.status}"
28
+ else
29
+ expect(last_response).not_to be_ok, "Expected a NOT OK response, got #{last_response.status}"
30
+ end
31
+ end
32
+
33
+ it "#{route}/new" do
34
+ get '/new'
35
+
36
+ if Pundit.policy(user, app.model_class).create?
37
+ expect(last_response).to be_ok, "Expected OK response, got #{last_response.status}"
38
+ else
39
+ expect(last_response).not_to be_ok, "Expected a NOT OK response, got #{last_response.status}"
40
+ end
41
+ end
42
+
43
+ it "#{route}/id" do
44
+ get "/#{model.id}"
45
+
46
+ if Pundit.policy(user, model).read?
47
+ expect(last_response).to be_ok, "Expected OK response, got #{last_response.status}"
48
+ else
49
+ expect(last_response).not_to be_ok, "Expected a NOT OK response, got #{last_response.status}"
50
+ end
51
+ end
52
+
53
+ it "#{route}/id/edit" do
54
+ get "/#{model.id}/edit"
55
+
56
+ if Pundit.policy(user, model).update?
57
+ expect(last_response).to be_ok, "Expected OK response, got #{last_response.status}"
58
+ else
59
+ expect(last_response).not_to be_ok, "Expected a NOT OK response, got #{last_response.status}"
60
+ end
61
+ end
62
+ end
63
+
64
+ context 'POST' do
65
+ it '/doesnotexist' do
66
+ header 'Accept', 'text/html'
67
+ post '/doesnotexist'
68
+ expect(last_response).not_to be_ok, "Expected a NOT OK response, got #{last_response.status}"
69
+ expect(last_response.status).to eq 404
70
+ end
71
+
72
+ it route.to_s do
73
+ header 'Accept', 'text/html'
74
+ post '/', create_data
75
+
76
+ if Pundit.policy(user, app.model_class).create?
77
+ expect(last_response.status).to eq 302
78
+ else
79
+ expect(last_response).not_to be_ok, "Expected a NOT OK response, got #{last_response.status}"
80
+ end
81
+ end
82
+
83
+ it "#{route} with invalid parameters" do
84
+ header 'Accept', 'text/html'
85
+ header 'Content-Type', 'application/x-www-form-urlencoded'
86
+ post '/', invalid_create_data
87
+
88
+ if Pundit.policy(user, app.model_class).create?
89
+ expect(last_response.status).to eq 400
90
+ else
91
+ expect(last_response).not_to be_ok, "Expected a NOT OK response, got #{last_response.status}"
92
+ end
93
+ end
94
+ end
95
+
96
+ context 'PUT' do
97
+ it '/doesnotexist' do
98
+ header 'Accept', 'text/html'
99
+ put '/doesnotexist'
100
+ expect(last_response).not_to be_ok, "Expected a NOT OK response, got #{last_response.status}"
101
+ expect(last_response.status).to eq 404
102
+ end
103
+
104
+ it "#{route}/:id" do
105
+ header 'Accept', 'text/html'
106
+ put "/#{model.id}", update_data
107
+
108
+ if Pundit.policy(user, model).update?
109
+ expect(last_response.status).to eq 302
110
+ else
111
+ expect(last_response).not_to be_ok, "Expected a NOT OK response, got #{last_response.status}"
112
+ end
113
+ end
114
+
115
+ it "#{route} with invalid parameters" do
116
+ header 'Accept', 'text/html'
117
+ put "/#{model.id}", invalid_update_data
118
+
119
+ if Pundit.policy(user, model).update?
120
+ expect(last_response.status).to eq 400
121
+ else
122
+ expect(last_response).not_to be_ok, "Expected a NOT OK response, got #{last_response.status}"
123
+ end
124
+ end
125
+ end
126
+
127
+ context 'DELETE' do
128
+ it '/doesnotexist' do
129
+ delete '/doesnotexist'
130
+ expect(last_response).not_to be_ok, "Expected a NOT OK response, got #{last_response.status}"
131
+ expect(last_response.status).to eq 404
132
+ end
133
+
134
+ it "#{route}/id" do
135
+ header 'Accept', 'text/html'
136
+ delete "/#{model.id}"
137
+
138
+ if Pundit.policy(user, model).delete?
139
+ expect(last_response.status).to eq 302
140
+ else
141
+ expect(last_response).not_to be_ok
142
+ end
143
+ end
144
+ end
145
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ditty
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jurgens du Toit
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-03-22 00:00:00.000000000 Z
11
+ date: 2020-03-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -621,6 +621,25 @@ files:
621
621
  - public/images/safari-pinned-tab.svg
622
622
  - public/js/scripts.js
623
623
  - public/manifest.json
624
+ - spec/ditty/api_spec.rb
625
+ - spec/ditty/controllers/roles_spec.rb
626
+ - spec/ditty/controllers/user_login_traits_spec.rb
627
+ - spec/ditty/controllers/users_spec.rb
628
+ - spec/ditty/emails/base_spec.rb
629
+ - spec/ditty/emails/forgot_password_spec.rb
630
+ - spec/ditty/helpers/component_spec.rb
631
+ - spec/ditty/models/user_spec.rb
632
+ - spec/ditty/services/email_spec.rb
633
+ - spec/ditty/services/logger_spec.rb
634
+ - spec/ditty/services/settings_spec.rb
635
+ - spec/ditty_spec.rb
636
+ - spec/factories.rb
637
+ - spec/fixtures/logger.yml
638
+ - spec/fixtures/section.yml
639
+ - spec/fixtures/settings.yml
640
+ - spec/spec_helper.rb
641
+ - spec/support/api_shared_examples.rb
642
+ - spec/support/crud_shared_examples.rb
624
643
  - views/400.haml
625
644
  - views/403.haml
626
645
  - views/404.haml