omu-support 0.1.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.
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'minitest/association_helper'
4
+ require_relative 'minitest/callback_helper'
5
+ require_relative 'minitest/enumeration_helper'
6
+ require_relative 'minitest/validation_helper'
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMU
4
+ module Support
5
+ module Minitest
6
+ module AssociationHelper
7
+ SUFFIX = 'Test'
8
+ RELATIONS = %i[belongs_to has_many has_one has_and_belongs_to_many].freeze
9
+
10
+ RELATIONS.each do |relation|
11
+ define_method(relation) do |attribute, **options|
12
+ association = relations_for(relation, attribute)
13
+
14
+ test "can respond to #{relation} #{attribute}" do
15
+ assert association
16
+ options.each do |key, value|
17
+ assert_equal association.options[key], value, "Option: #{key}"
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ def accepts_nested_attributes_for(attribute, **options)
24
+ nested_attributes_options = klass.nested_attributes_options[attribute]
25
+
26
+ test "#{attribute} must be nested attribute" do
27
+ assert nested_attributes_options
28
+ options.each do |key, value|
29
+ assert_equal nested_attributes_options[key], value, "Option: #{key}"
30
+ end
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def relations_for(relation, attribute)
37
+ klass.reflect_on_all_associations(relation).find do |association|
38
+ association.name == attribute
39
+ end
40
+ end
41
+
42
+ def klass
43
+ to_s.delete_suffix(SUFFIX).constantize
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMU
4
+ module Support
5
+ module Minitest
6
+ module CallbackHelper
7
+ CALLBACKS = %i[
8
+ after_commit
9
+ after_create
10
+ after_destroy
11
+ after_find
12
+ after_initialize
13
+ after_rollback
14
+ after_save
15
+ after_touch
16
+ after_update
17
+ after_validation
18
+ around_create
19
+ around_destroy
20
+ around_save
21
+ around_update
22
+ before_create
23
+ before_destroy
24
+ before_save
25
+ before_update
26
+ before_validation
27
+ ].freeze
28
+
29
+ CALLBACKS.each do |callback|
30
+ define_method(callback) do |action|
31
+ test "has #{callback} for #{action}" do
32
+ kind, method = callback.to_s.split('_')
33
+ klass = class_name.delete_suffix('Test').constantize
34
+ callbacks = klass.public_send("_#{method}_callbacks")
35
+ assert callbacks.select { |cb| cb.filter.eql?(action.to_sym) && cb.kind.eql?(kind.to_sym) }.any?
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMU
4
+ module Support
5
+ module Minitest
6
+ module EnumerationHelper
7
+ def enum(definitions)
8
+ definitions.each do |attribute, values|
9
+ values.each do |key, value|
10
+ test "has a enum key (#{key}) with a value of #{value}" do
11
+ klass = class_name.delete_suffix('Test').constantize
12
+ defined_value = klass.defined_enums.dig(attribute.to_s, key.to_s)
13
+ assert_equal defined_value, value, "Enum: #{attribute}"
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMU
4
+ module Support
5
+ module Minitest
6
+ module ValidationHelper
7
+ SUFFIX = 'Test'
8
+
9
+ def validates_presence_of(*attributes)
10
+ attributes.each do |attribute|
11
+ test "#{attribute} must be present (presence: true)" do
12
+ object.public_send("#{attribute}=", nil)
13
+ assert_not object.valid?
14
+ assert_not_empty object.errors[attribute]
15
+ end
16
+ end
17
+ end
18
+
19
+ def validates_presence_of_nested_model(attribute, ids: nil)
20
+ test "nested model (#{attribute}) must be present" do
21
+ ids ||= "#{attribute.to_s.singularize}_ids"
22
+ object.public_send("#{ids}=", nil)
23
+ assert_not object.valid?
24
+ assert_not_empty object.errors[attribute]
25
+ end
26
+ end
27
+
28
+ def validates_uniqueness_of(*attributes)
29
+ attributes.each do |attribute|
30
+ test "#{attribute} must be unique (uniqueness: true)" do
31
+ duplicate_object = object.dup
32
+ assert_not duplicate_object.valid?
33
+ assert_not_empty duplicate_object.errors[attribute]
34
+ end
35
+ end
36
+ end
37
+
38
+ def validates_length_of(attribute, **option) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
39
+ option[:maximum] = 255 if option.blank?
40
+
41
+ controls = {
42
+ is: { value: 1, error_key: :wrong_length },
43
+ minimum: { value: -1, error_key: :too_short },
44
+ maximum: { value: 1, error_key: :too_long }
45
+ }
46
+
47
+ key = option.keys.first
48
+ value = option[key].to_i + controls.dig(key, :value).to_i
49
+ error_key = controls.dig(key, :error_key)
50
+
51
+ test "#{attribute} length must be #{option}" do
52
+ object.public_send("#{attribute}=", (0..value).map { ('a'..'z').to_a[rand(26)] }.join)
53
+ assert_not object.valid?
54
+ assert object.errors.details[attribute].pluck(:error).include?(error_key)
55
+ end
56
+ end
57
+
58
+ def validates_numericality_of(attribute)
59
+ test "#{attribute} must be a number" do
60
+ object.public_send("#{attribute}=", 'some string')
61
+ assert_not object.valid?
62
+ assert object.errors.details[attribute].pluck(:error).include?(:not_a_number)
63
+ end
64
+ end
65
+
66
+ def validates_numerical_range(attribute, **option) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
67
+ controls = {
68
+ greater_than: 0,
69
+ less_than: 0,
70
+ greater_than_or_equal_to: -1,
71
+ less_than_or_equal_to: 1
72
+ }
73
+
74
+ key = option.keys.first
75
+ value = option[key].to_i + controls[key].to_i
76
+
77
+ test "#{attribute} must be #{key} #{value}" do
78
+ object.public_send("#{attribute}=", value)
79
+ assert_not object.valid?
80
+ assert object.errors.details[attribute].pluck(:error).include?(key)
81
+ end
82
+ end
83
+
84
+ def self.extended(base)
85
+ super
86
+
87
+ base.define_method :object do
88
+ @object ||= class_name.delete_suffix(SUFFIX).constantize.take
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+
5
+ module OMU
6
+ module Support
7
+ module RestClient
8
+ SUPPORTED_HTTP_METHODS = %i[
9
+ delete
10
+ get
11
+ patch
12
+ post
13
+ put
14
+ ].freeze
15
+
16
+ private_constant :SUPPORTED_HTTP_METHODS
17
+
18
+ class Error < StandardError; end
19
+
20
+ class HTTPMethodError < Error; end
21
+
22
+ class UnsupportedHTTPOptionError < Error; end
23
+
24
+ class HTTPError < Error; end
25
+
26
+ class Request
27
+ SUPPORTED_HTTP_OPTIONS = {
28
+ open_timeout: 10,
29
+ read_timeout: 10,
30
+ use_ssl: false,
31
+ verify_mode: OpenSSL::SSL::VERIFY_NONE
32
+ }.freeze
33
+
34
+ HEADERS = {
35
+ 'Content-Type' => 'application/json'
36
+ }.freeze
37
+
38
+ private_constant :SUPPORTED_HTTP_OPTIONS, :HEADERS
39
+
40
+ # rubocop:disable Style/IfUnlessModifier
41
+ def initialize(method, url, headers = {}, **http_options)
42
+ @method = method
43
+ @url = url
44
+ @headers = HEADERS.merge(headers)
45
+
46
+ unless method.in?(SUPPORTED_HTTP_METHODS)
47
+ raise HTTPMethodError, "unsupported HTTP method: #{method}"
48
+ end
49
+
50
+ build_http_object http_options
51
+ end
52
+ # rubocop:enable Style/IfUnlessModifier
53
+
54
+ def execute(payload = nil)
55
+ klass = "Net::HTTP::#{method.capitalize}".constantize
56
+ request = klass.new url, headers
57
+ Response.new @http.request(request, payload)
58
+ end
59
+
60
+ protected
61
+
62
+ attr_reader :method, :url, :headers
63
+
64
+ private
65
+
66
+ def build_http_object(http_options)
67
+ uri = URI.parse(url)
68
+ @http = Net::HTTP.new uri.host, uri.port
69
+
70
+ unsupported_options = http_options.keys - SUPPORTED_HTTP_OPTIONS.keys
71
+ unless unsupported_options.empty?
72
+ raise UnsupportedHTTPOptionError, "unsupported HTTP options: #{unsupported_options}"
73
+ end
74
+
75
+ http_options.each { |option, value| @http.public_send "#{option}=", value }
76
+ end
77
+ end
78
+
79
+ private_constant :Request
80
+
81
+ Response = Struct.new(:http_response) do
82
+ delegate :body, to: :http_response
83
+
84
+ def decode
85
+ body && JSON.parse(body, symbolize_names: true)
86
+ rescue JSON::JSONError => e
87
+ Rails.logger.warn("JSON parse error: #{e}")
88
+ nil
89
+ end
90
+
91
+ def error!
92
+ http_response.error! unless ok?
93
+ rescue Net::HTTPError, Net::HTTPFatalError => e
94
+ Rails.logger.warn("HTTP error: #{e}")
95
+ raise HTTPError
96
+ end
97
+
98
+ def ok?
99
+ http_response.is_a?(Net::HTTPOK)
100
+ end
101
+ end
102
+
103
+ private_constant :Response
104
+
105
+ module_function
106
+
107
+ SUPPORTED_HTTP_METHODS.each do |method|
108
+ define_method(method) do |url, headers: {}, payload: nil, **http_options|
109
+ Request.new(method, url, headers, **http_options).execute(payload)
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module OMU
6
+ module Support
7
+ # Sensitive file read/write
8
+ module Sensitive
9
+ module_function
10
+
11
+ def read(path)
12
+ encryptor(expand_path(path)).read
13
+ end
14
+
15
+ def readlines(path)
16
+ read(path).split "\n"
17
+ end
18
+
19
+ def write(path, content)
20
+ unless Dir.exist?(dir = File.dirname(file = expand_path(path)))
21
+ FileUtils.mkdir_p(dir)
22
+ end
23
+ encryptor(file).write(content)
24
+ end
25
+
26
+ def writelines(path, contents)
27
+ write(path, contents.join("\n"))
28
+ end
29
+
30
+ def read_write(path)
31
+ write(path, File.read(path))
32
+ end
33
+
34
+ def content_decrypt(content)
35
+ encryptor('').__send__(:decrypt, content)
36
+ end
37
+
38
+ EXT = '.enc'
39
+
40
+ def expand_path(path)
41
+ File.expand_path(path, Rails.root) + EXT
42
+ end
43
+
44
+ def encryptor(file)
45
+ ActiveSupport::EncryptedFile.new(
46
+ content_path: file,
47
+ env_key: 'RAILS_MASTER_KEY',
48
+ key_path: Rails.root.join('config/master.key'),
49
+ raise_if_missing_key: true
50
+ )
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMU
4
+ module Support
5
+ VERSION = '0.1.1'
6
+ end
7
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class ArrayTest < ActiveSupport::TestCase
6
+ test 'clip works' do
7
+ assert_equal %w[foo bar], %w[foo bar baz quux].clip(2)
8
+ end
9
+
10
+ test 'join_affixed works' do
11
+ assert_equal 'aaa.foo-bar-baz-quux_zzz',
12
+ %w[foo bar baz quux].join_affixed(prefix: 'aaa.', interfix: '-', suffix: '_zzz')
13
+ end
14
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class ClassTest < ActiveSupport::TestCase
6
+ module TestClassesOne
7
+ class NonConveyedBase
8
+ class_attribute :array_attr, default: %w[foo bar]
9
+ class_attribute :hash_attr, default: { x: 'foo', y: 'bar' }
10
+ end
11
+
12
+ class ConveyedBase
13
+ class_attribute :array_attr, default: %w[foo bar]
14
+ class_attribute :hash_attr, default: { x: 'foo', y: 'bar' }
15
+
16
+ inherited_by_conveying_attributes :array_attr, :hash_attr
17
+ end
18
+
19
+ class ChildFromNonConveyedBase < NonConveyedBase
20
+ array_attr << 'child_addition'
21
+ hash_attr.merge! z: 'child_addition'
22
+ end
23
+
24
+ class ChildFromConveyedBase < ConveyedBase
25
+ array_attr << 'child_addition'
26
+ hash_attr.merge! z: 'child_addition'
27
+ end
28
+ end
29
+
30
+ test 'inherited_by_conveying_attributes works' do
31
+ assert_equal %w[foo bar child_addition], TestClassesOne::NonConveyedBase.array_attr
32
+ assert_equal %w[foo bar], TestClassesOne::ConveyedBase.array_attr
33
+ assert_equal %w[foo bar child_addition], TestClassesOne::ChildFromNonConveyedBase.array_attr
34
+ assert_equal %w[foo bar child_addition], TestClassesOne::ChildFromConveyedBase.array_attr
35
+
36
+ assert_equal Hash[x: 'foo', y: 'bar', z: 'child_addition'], TestClassesOne::NonConveyedBase.hash_attr
37
+ assert_equal Hash[x: 'foo', y: 'bar'], TestClassesOne::ConveyedBase.hash_attr
38
+ assert_equal Hash[x: 'foo', y: 'bar', z: 'child_addition'], TestClassesOne::ChildFromNonConveyedBase.hash_attr
39
+ assert_equal Hash[x: 'foo', y: 'bar', z: 'child_addition'], TestClassesOne::ChildFromConveyedBase.hash_attr
40
+ end
41
+
42
+ module TestClassesTwo
43
+ class ConveyedBase
44
+ class_attribute :array_attr, default: %w[foo bar]
45
+
46
+ inherited_by_conveying_attributes :array_attr do |child|
47
+ child.class_attribute :other, default: 'ok'
48
+ end
49
+ end
50
+
51
+ class ChildFromConveyedBase < ConveyedBase
52
+ array_attr << 'child_addition'
53
+ end
54
+ end
55
+
56
+ test 'inherited_by_conveying_attributes with block works' do
57
+ assert_equal %w[foo bar], TestClassesTwo::ConveyedBase.array_attr
58
+ assert_equal %w[foo bar child_addition], TestClassesTwo::ChildFromConveyedBase.array_attr
59
+
60
+ assert_not TestClassesTwo::ConveyedBase.respond_to? :other
61
+ assert_equal 'ok', TestClassesTwo::ChildFromConveyedBase.other
62
+ end
63
+ end