ditty 0.9.0 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
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