familia 2.0.0.pre12 → 2.0.0.pre14
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 +4 -4
- data/.rubocop_todo.yml +2 -3
- data/CHANGELOG.rst +529 -0
- data/CLAUDE.md +1 -1
- data/Gemfile +1 -6
- data/Gemfile.lock +13 -7
- data/README.md +21 -2
- data/changelog.d/README.md +5 -5
- data/{setup.cfg → changelog.d/scriv.ini} +1 -1
- data/docs/guides/Feature-System-Autoloading.md +228 -0
- data/docs/guides/time-utilities.md +221 -0
- data/docs/migrating/v2.0.0-pre11.md +14 -16
- data/docs/migrating/v2.0.0-pre13.md +95 -0
- data/docs/migrating/v2.0.0-pre14.md +37 -0
- data/examples/autoloader/mega_customer/safe_dump_fields.rb +6 -0
- data/examples/autoloader/mega_customer.rb +17 -0
- data/examples/safe_dump.rb +1 -1
- data/familia.gemspec +1 -0
- data/lib/familia/autoloader.rb +53 -0
- data/lib/familia/base.rb +5 -0
- data/lib/familia/data_type.rb +4 -0
- data/lib/familia/encryption/encrypted_data.rb +4 -4
- data/lib/familia/encryption/manager.rb +6 -4
- data/lib/familia/encryption.rb +1 -1
- data/lib/familia/errors.rb +3 -0
- data/lib/familia/features/autoloadable.rb +113 -0
- data/lib/familia/features/encrypted_fields/concealed_string.rb +4 -2
- data/lib/familia/features/expiration.rb +4 -0
- data/lib/familia/features/external_identifier.rb +3 -3
- data/lib/familia/features/quantization.rb +5 -0
- data/lib/familia/features/safe_dump.rb +7 -0
- data/lib/familia/features.rb +20 -16
- data/lib/familia/field_type.rb +2 -0
- data/lib/familia/horreum/core/serialization.rb +3 -3
- data/lib/familia/horreum/subclass/definition.rb +3 -4
- data/lib/familia/horreum.rb +2 -0
- data/lib/familia/json_serializer.rb +70 -0
- data/lib/familia/logging.rb +12 -10
- data/lib/familia/refinements/logger_trace.rb +57 -0
- data/lib/familia/refinements/snake_case.rb +40 -0
- data/lib/familia/refinements/time_literals.rb +279 -0
- data/lib/familia/refinements.rb +3 -49
- data/lib/familia/utils.rb +2 -0
- data/lib/familia/validation/{test_helpers.rb → validation_helpers.rb} +2 -2
- data/lib/familia/validation.rb +1 -1
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +15 -3
- data/try/core/autoloader_try.rb +112 -0
- data/try/core/extensions_try.rb +38 -21
- data/try/core/familia_extended_try.rb +4 -3
- data/try/core/time_utils_try.rb +130 -0
- data/try/data_types/datatype_base_try.rb +3 -2
- data/try/features/autoloadable/autoloadable_try.rb +61 -0
- data/try/features/encrypted_fields/concealed_string_core_try.rb +8 -3
- data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +59 -17
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +36 -12
- data/try/features/external_identifier/external_identifier_try.rb +26 -0
- data/try/features/feature_improvements_try.rb +2 -1
- data/try/features/real_feature_integration_try.rb +1 -1
- data/try/features/safe_dump/safe_dump_autoloading_try.rb +111 -0
- data/try/helpers/test_helpers.rb +24 -0
- data/try/integration/cross_component_try.rb +3 -1
- metadata +34 -6
- data/CHANGELOG.md +0 -247
- data/lib/familia/core_ext.rb +0 -135
- data/lib/familia/features/autoloader.rb +0 -57
@@ -0,0 +1,112 @@
|
|
1
|
+
# try/core/autoloader_try.rb
|
2
|
+
|
3
|
+
require_relative '../../lib/familia'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'tmpdir'
|
6
|
+
|
7
|
+
# Create test directory structure for Autoloader testing
|
8
|
+
@test_dir = Dir.mktmpdir('familia_autoloader_test')
|
9
|
+
@features_dir = File.join(@test_dir, 'features')
|
10
|
+
@test_file1 = File.join(@features_dir, 'test_feature1.rb')
|
11
|
+
@test_file2 = File.join(@features_dir, 'test_feature2.rb')
|
12
|
+
@excluded_file = File.join(@features_dir, 'autoloader.rb')
|
13
|
+
|
14
|
+
# Create directory structure
|
15
|
+
FileUtils.mkdir_p(@features_dir)
|
16
|
+
|
17
|
+
# Write test files
|
18
|
+
File.write(@test_file1, <<~RUBY)
|
19
|
+
# Test feature file 1
|
20
|
+
$test_feature1_loaded = true
|
21
|
+
RUBY
|
22
|
+
|
23
|
+
File.write(@test_file2, <<~RUBY)
|
24
|
+
# Test feature file 2
|
25
|
+
$test_feature2_loaded = true
|
26
|
+
RUBY
|
27
|
+
|
28
|
+
File.write(@excluded_file, <<~RUBY)
|
29
|
+
# This should be excluded
|
30
|
+
$autoloader_file_loaded = true
|
31
|
+
RUBY
|
32
|
+
|
33
|
+
## Test that Familia::Autoloader exists and is a module
|
34
|
+
Familia::Autoloader.is_a?(Module)
|
35
|
+
#=> true
|
36
|
+
|
37
|
+
## Test that autoload_files class method exists
|
38
|
+
Familia::Autoloader.respond_to?(:autoload_files)
|
39
|
+
#=> true
|
40
|
+
|
41
|
+
## Test that included class method exists
|
42
|
+
Familia::Autoloader.respond_to?(:included)
|
43
|
+
#=> true
|
44
|
+
|
45
|
+
## Test autoload_files with single pattern
|
46
|
+
$test_feature1_loaded = false
|
47
|
+
$test_feature2_loaded = false
|
48
|
+
$autoloader_file_loaded = false
|
49
|
+
|
50
|
+
Familia::Autoloader.autoload_files(File.join(@features_dir, '*.rb'))
|
51
|
+
$test_feature1_loaded && $test_feature2_loaded
|
52
|
+
#=> true
|
53
|
+
|
54
|
+
## Test that autoload_files respects exclusions (using fresh files)
|
55
|
+
@exclude_test_dir = Dir.mktmpdir('familia_autoloader_exclude_test')
|
56
|
+
@exclude_features_dir = File.join(@exclude_test_dir, 'features')
|
57
|
+
@include_file = File.join(@exclude_features_dir, 'include_me.rb')
|
58
|
+
@exclude_file = File.join(@exclude_features_dir, 'autoloader.rb')
|
59
|
+
|
60
|
+
FileUtils.mkdir_p(@exclude_features_dir)
|
61
|
+
File.write(@include_file, '$include_me_loaded = true')
|
62
|
+
File.write(@exclude_file, '$exclude_me_loaded = true')
|
63
|
+
|
64
|
+
$include_me_loaded = false
|
65
|
+
$exclude_me_loaded = false
|
66
|
+
|
67
|
+
Familia::Autoloader.autoload_files(
|
68
|
+
File.join(@exclude_features_dir, '*.rb'),
|
69
|
+
exclude: ['autoloader.rb']
|
70
|
+
)
|
71
|
+
|
72
|
+
# Should load include file but not the excluded one
|
73
|
+
$include_me_loaded && !$exclude_me_loaded
|
74
|
+
#=> true
|
75
|
+
|
76
|
+
## Test autoload_files with array of patterns (using fresh files)
|
77
|
+
@pattern_test_dir = Dir.mktmpdir('familia_autoloader_pattern_test')
|
78
|
+
@pattern_dir1 = File.join(@pattern_test_dir, 'dir1')
|
79
|
+
@pattern_dir2 = File.join(@pattern_test_dir, 'dir2')
|
80
|
+
@pattern_file1 = File.join(@pattern_dir1, 'file1.rb')
|
81
|
+
@pattern_file2 = File.join(@pattern_dir2, 'file2.rb')
|
82
|
+
|
83
|
+
FileUtils.mkdir_p(@pattern_dir1)
|
84
|
+
FileUtils.mkdir_p(@pattern_dir2)
|
85
|
+
File.write(@pattern_file1, '$pattern1_loaded = true')
|
86
|
+
File.write(@pattern_file2, '$pattern2_loaded = true')
|
87
|
+
|
88
|
+
$pattern1_loaded = false
|
89
|
+
$pattern2_loaded = false
|
90
|
+
|
91
|
+
Familia::Autoloader.autoload_files([
|
92
|
+
File.join(@pattern_dir1, '*.rb'),
|
93
|
+
File.join(@pattern_dir2, '*.rb')
|
94
|
+
])
|
95
|
+
|
96
|
+
$pattern1_loaded && $pattern2_loaded
|
97
|
+
#=> true
|
98
|
+
|
99
|
+
## Test that included method loads features from features directory
|
100
|
+
# Create a mock module that includes Autoloader
|
101
|
+
@mock_features_module = Module.new do
|
102
|
+
include Familia::Autoloader
|
103
|
+
end
|
104
|
+
|
105
|
+
# The Features module already includes Autoloader, so test indirectly
|
106
|
+
Familia::Features.ancestors.include?(Familia::Autoloader)
|
107
|
+
#=> true
|
108
|
+
|
109
|
+
# Cleanup test files and directories
|
110
|
+
FileUtils.rm_rf(@test_dir)
|
111
|
+
FileUtils.rm_rf(@exclude_test_dir)
|
112
|
+
FileUtils.rm_rf(@pattern_test_dir)
|
data/try/core/extensions_try.rb
CHANGED
@@ -1,59 +1,76 @@
|
|
1
1
|
require_relative '../helpers/test_helpers'
|
2
2
|
|
3
|
+
module RefinedContext
|
4
|
+
using Familia::Refinements::TimeLiterals
|
5
|
+
|
6
|
+
# This helper evaluates code within the refined context using eval.
|
7
|
+
# This works because eval executes the code as if it were written
|
8
|
+
# at this location, making the refinements available.
|
9
|
+
def self.eval_in_refined_context(code)
|
10
|
+
eval(code)
|
11
|
+
end
|
12
|
+
|
13
|
+
# This helper also evaluates code in the refined context using instance_eval.
|
14
|
+
# This provides an alternative approach for testing refinements.
|
15
|
+
def self.instance_eval_in_refined_context(code)
|
16
|
+
instance_eval(code)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
3
20
|
# Test core extensions
|
4
21
|
|
5
22
|
## String time parsing - seconds
|
6
|
-
'60s'.in_seconds
|
7
|
-
#=> 60
|
23
|
+
RefinedContext.eval_in_refined_context("'60s'.in_seconds")
|
24
|
+
#=> 60.0
|
8
25
|
|
9
26
|
## String time parsing - minutes
|
10
|
-
'5m'.in_seconds
|
11
|
-
#=> 300
|
27
|
+
RefinedContext.instance_eval_in_refined_context("'5m'.in_seconds")
|
28
|
+
#=> 300.0
|
12
29
|
|
13
30
|
## String time parsing - hours
|
14
|
-
'2h'.in_seconds
|
15
|
-
#=> 7200
|
31
|
+
RefinedContext.eval_in_refined_context("'2h'.in_seconds")
|
32
|
+
#=> 7200.0
|
16
33
|
|
17
34
|
## String time parsing - days
|
18
|
-
'1d'.in_seconds
|
19
|
-
#=> 86_400
|
35
|
+
RefinedContext.instance_eval_in_refined_context("'1d'.in_seconds")
|
36
|
+
#=> 86_400.0
|
20
37
|
|
21
|
-
## String time parsing -
|
22
|
-
'1y'.in_seconds
|
23
|
-
#=>
|
38
|
+
## String time parsing - years
|
39
|
+
RefinedContext.eval_in_refined_context("'1y'.in_seconds")
|
40
|
+
#=> 31556952.0
|
24
41
|
|
25
42
|
## Time::Units - second
|
26
|
-
1.second
|
43
|
+
RefinedContext.instance_eval_in_refined_context("1.second")
|
27
44
|
#=> 1
|
28
45
|
|
29
46
|
## Time::Units - minute
|
30
|
-
1.minute
|
47
|
+
RefinedContext.eval_in_refined_context("1.minute")
|
31
48
|
#=> 60
|
32
49
|
|
33
50
|
## Time::Units - hour
|
34
|
-
1.hour
|
51
|
+
RefinedContext.instance_eval_in_refined_context("1.hour")
|
35
52
|
#=> 3600
|
36
53
|
|
37
54
|
## Time::Units - day
|
38
|
-
1.day
|
55
|
+
RefinedContext.eval_in_refined_context("1.day")
|
39
56
|
#=> 86_400
|
40
57
|
|
41
58
|
## Time::Units - week
|
42
|
-
1.week
|
59
|
+
RefinedContext.instance_eval_in_refined_context("1.week")
|
43
60
|
#=> 604_800
|
44
61
|
|
45
62
|
## Numeric extension to_ms
|
46
|
-
1000.to_ms
|
47
|
-
#=>
|
63
|
+
RefinedContext.eval_in_refined_context("1000.to_ms")
|
64
|
+
#=> 1000000.0
|
48
65
|
|
49
66
|
## Numeric extension to_bytes - single byte
|
50
|
-
1.to_bytes
|
67
|
+
RefinedContext.instance_eval_in_refined_context("1.to_bytes")
|
51
68
|
#=> '1.00 B'
|
52
69
|
|
53
70
|
## Numeric extension to_bytes - kilobytes
|
54
|
-
1024.to_bytes
|
71
|
+
RefinedContext.eval_in_refined_context("1024.to_bytes")
|
55
72
|
#=> '1.00 KiB'
|
56
73
|
|
57
74
|
## Numeric extension to_bytes - megabytes
|
58
|
-
(1024 * 1024).to_bytes
|
75
|
+
RefinedContext.instance_eval_in_refined_context("(1024 * 1024).to_bytes")
|
59
76
|
#=> '1.00 MiB'
|
@@ -44,14 +44,15 @@ parsed_time = Familia.now(Time.parse('2011-04-10 20:56:20 UTC').utc)
|
|
44
44
|
#=> [1302468980.0, true, true]
|
45
45
|
|
46
46
|
## Familia.qnow
|
47
|
-
Familia.qstamp 10.minutes, time: 1_302_468_980
|
47
|
+
RefinedContext.eval_in_refined_context("Familia.qstamp 10.minutes, time: 1_302_468_980")
|
48
48
|
#=> 1302468600
|
49
49
|
|
50
50
|
## Familia::Object.qstamp
|
51
|
-
Limiter.qstamp(10.minutes, pattern: '%H:%M', time: 1_302_468_980)
|
51
|
+
RefinedContext.eval_in_refined_context("Limiter.qstamp(10.minutes, pattern: '%H:%M', time: 1_302_468_980)")
|
52
52
|
#=> '20:50'
|
53
53
|
|
54
54
|
## Familia::Object#qstamp
|
55
55
|
limiter = Limiter.new :request
|
56
|
-
|
56
|
+
RefinedContext.instance_variable_set(:@limiter, limiter)
|
57
|
+
RefinedContext.eval_in_refined_context("@limiter.qstamp(10.minutes, pattern: '%H:%M', time: 1_302_468_980)")
|
57
58
|
#=> '20:50'
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require_relative '../helpers/test_helpers'
|
2
|
+
|
3
|
+
module RefinedContext
|
4
|
+
using Familia::Refinements::TimeLiterals
|
5
|
+
|
6
|
+
def self.eval_in_refined_context(code)
|
7
|
+
eval(code)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.instance_eval_in_refined_context(code)
|
11
|
+
instance_eval(code)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Test TimeLiterals refinement
|
16
|
+
|
17
|
+
## Numeric#months - convert number to months in seconds
|
18
|
+
result = RefinedContext.eval_in_refined_context("1.month")
|
19
|
+
result.round(0)
|
20
|
+
#=> 2629746.0
|
21
|
+
|
22
|
+
## Numeric#months - plural form
|
23
|
+
result = RefinedContext.instance_eval_in_refined_context("2.months")
|
24
|
+
result.round(0)
|
25
|
+
#=> 5259492.0
|
26
|
+
|
27
|
+
## Numeric#years - convert number to years in seconds
|
28
|
+
result = RefinedContext.eval_in_refined_context("1.year")
|
29
|
+
result.round(0)
|
30
|
+
#=> 31556952.0
|
31
|
+
|
32
|
+
## Numeric#in_months - convert seconds to months
|
33
|
+
RefinedContext.instance_eval_in_refined_context("2629746.in_months")
|
34
|
+
#=> 1.0
|
35
|
+
|
36
|
+
## Numeric#in_years - convert seconds to years
|
37
|
+
result = RefinedContext.eval_in_refined_context("#{Familia::Refinements::TimeLiterals::PER_YEAR}.in_years")
|
38
|
+
result.round(1)
|
39
|
+
#=> 1.0
|
40
|
+
|
41
|
+
## String#in_seconds - parse month string
|
42
|
+
RefinedContext.instance_eval_in_refined_context("'1mo'.in_seconds")
|
43
|
+
#=> 2629746.0
|
44
|
+
|
45
|
+
## String#in_seconds - parse month string (long form)
|
46
|
+
RefinedContext.eval_in_refined_context("'2months'.in_seconds")
|
47
|
+
#=> 5259492.0
|
48
|
+
|
49
|
+
## String#in_seconds - parse year string
|
50
|
+
result = RefinedContext.instance_eval_in_refined_context("'1y'.in_seconds")
|
51
|
+
result.round(0)
|
52
|
+
#=> 31556952.0
|
53
|
+
|
54
|
+
## Numeric#age_in - calculate age in months from timestamp (approximately 1 month ago)
|
55
|
+
timestamp = Time.now.to_f - Familia::Refinements::TimeLiterals::PER_MONTH
|
56
|
+
result = RefinedContext.eval_in_refined_context("#{timestamp}.age_in(:months)")
|
57
|
+
(result - 1.0).abs < 0.01
|
58
|
+
#=> true
|
59
|
+
|
60
|
+
## Numeric#age_in - calculate age in years from timestamp (approximately 1 year ago)
|
61
|
+
timestamp = Time.now.to_f - Familia::Refinements::TimeLiterals::PER_YEAR
|
62
|
+
result = RefinedContext.instance_eval_in_refined_context("#{timestamp}.age_in(:years)")
|
63
|
+
(result - 1.0).abs < 0.01
|
64
|
+
#=> true
|
65
|
+
|
66
|
+
## Numeric#months_old - convenience method for age_in(:months)
|
67
|
+
timestamp = Time.now.to_f - Familia::Refinements::TimeLiterals::PER_MONTH
|
68
|
+
result = RefinedContext.eval_in_refined_context("#{timestamp}.months_old")
|
69
|
+
(result - 1.0).abs < 0.01
|
70
|
+
#=> true
|
71
|
+
|
72
|
+
## Numeric#years_old - convenience method for age_in(:years)
|
73
|
+
timestamp = Time.now.to_f - Familia::Refinements::TimeLiterals::PER_YEAR
|
74
|
+
result = RefinedContext.instance_eval_in_refined_context("#{timestamp}.years_old")
|
75
|
+
(result - 1.0).abs < 0.01
|
76
|
+
#=> true
|
77
|
+
|
78
|
+
## Numeric#months_old - should NOT return seconds (the original bug)
|
79
|
+
timestamp = Time.now.to_f - Familia::Refinements::TimeLiterals::PER_MONTH
|
80
|
+
result = RefinedContext.eval_in_refined_context("#{timestamp}.months_old")
|
81
|
+
result.between?(0.9, 1.1) # Should be ~1 month, not millions of seconds
|
82
|
+
#=> true
|
83
|
+
|
84
|
+
## Numeric#years_old - should NOT return seconds (the original bug)
|
85
|
+
timestamp = Time.now.to_f - Familia::Refinements::TimeLiterals::PER_YEAR
|
86
|
+
result = RefinedContext.instance_eval_in_refined_context("#{timestamp}.years_old")
|
87
|
+
result.between?(0.9, 1.1) # Should be ~1 year, not millions of seconds
|
88
|
+
#=> true
|
89
|
+
|
90
|
+
## age_in with from_time parameter - months
|
91
|
+
past_time = Time.now - (2 * Familia::Refinements::TimeLiterals::PER_MONTH) # 2 months ago
|
92
|
+
from_time = Time.now - Familia::Refinements::TimeLiterals::PER_MONTH # 1 month ago
|
93
|
+
result = RefinedContext.eval_in_refined_context("#{past_time.to_f}.age_in(:months, #{from_time.to_f})")
|
94
|
+
(result - 1.0).abs < 0.01
|
95
|
+
#=> true
|
96
|
+
|
97
|
+
## age_in with from_time parameter - years
|
98
|
+
past_time = Time.now - (2 * Familia::Refinements::TimeLiterals::PER_YEAR) # 2 years ago
|
99
|
+
from_time = Time.now - Familia::Refinements::TimeLiterals::PER_YEAR # 1 year ago
|
100
|
+
result = RefinedContext.instance_eval_in_refined_context("#{past_time.to_f}.age_in(:years, #{from_time.to_f})")
|
101
|
+
(result - 1.0).abs < 0.01
|
102
|
+
#=> true
|
103
|
+
|
104
|
+
## Verify month constant is approximately correct (30.437 days)
|
105
|
+
expected_seconds_per_month = 30.437 * 24 * 60 * 60
|
106
|
+
Familia::Refinements::TimeLiterals::PER_MONTH.round(0)
|
107
|
+
#=> 2629746.0
|
108
|
+
|
109
|
+
## Verify year constant (365.2425 days - Gregorian year)
|
110
|
+
expected_seconds_per_year = 365.2425 * 24 * 60 * 60
|
111
|
+
Familia::Refinements::TimeLiterals::PER_YEAR.round(0)
|
112
|
+
#=> 31556952.0
|
113
|
+
|
114
|
+
## UNIT_METHODS contains months mapping
|
115
|
+
Familia::Refinements::TimeLiterals::UNIT_METHODS['months']
|
116
|
+
#=> :months
|
117
|
+
|
118
|
+
## UNIT_METHODS contains mo mapping
|
119
|
+
Familia::Refinements::TimeLiterals::UNIT_METHODS['mo']
|
120
|
+
#=> :months
|
121
|
+
|
122
|
+
## UNIT_METHODS contains month mapping
|
123
|
+
Familia::Refinements::TimeLiterals::UNIT_METHODS['month']
|
124
|
+
#=> :months
|
125
|
+
|
126
|
+
## Calendar consistency - 12 months equals 1 year (fix for inconsistency issue)
|
127
|
+
result1 = RefinedContext.eval_in_refined_context("12.months")
|
128
|
+
result2 = RefinedContext.instance_eval_in_refined_context("1.year")
|
129
|
+
result1 == result2
|
130
|
+
#=> true
|
@@ -21,7 +21,7 @@ p [@a.name, @b.name]
|
|
21
21
|
#=> true
|
22
22
|
|
23
23
|
## Limiter#qstamp
|
24
|
-
@limiter1.counter.qstamp(10.minutes, '%H:%M', 1_302_468_980)
|
24
|
+
RefinedContext.eval_in_refined_context("@limiter1.counter.qstamp(10.minutes, '%H:%M', 1_302_468_980)")
|
25
25
|
##=> '20:50'
|
26
26
|
|
27
27
|
## Database Types can be stored to quantized stamp suffix
|
@@ -32,7 +32,8 @@ p [@a.name, @b.name]
|
|
32
32
|
@limiter2 = Limiter.new :requests
|
33
33
|
p [@limiter1.default_expiration, @limiter2.default_expiration]
|
34
34
|
p [@limiter1.counter.parent.default_expiration, @limiter2.counter.parent.default_expiration]
|
35
|
-
|
35
|
+
RefinedContext.instance_variable_set(:@limiter2, @limiter2)
|
36
|
+
RefinedContext.eval_in_refined_context("@limiter2.counter.qstamp(10.minutes, pattern: nil, time: 1_302_468_980)")
|
36
37
|
#=> 1302468600
|
37
38
|
|
38
39
|
## Database Types can be stored to quantized numeric suffix. This
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# try/features/autoloadable/autoloadable_try.rb
|
2
|
+
|
3
|
+
require_relative '../../../lib/familia'
|
4
|
+
|
5
|
+
# Create test feature module that includes Autoloadable
|
6
|
+
module TestAutoloadableFeature
|
7
|
+
include Familia::Features::Autoloadable
|
8
|
+
|
9
|
+
def self.included(base)
|
10
|
+
super
|
11
|
+
base.define_method(:test_feature_method) { "feature_loaded" }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Create test class to include the feature
|
16
|
+
class TestModelForAutoloadable < Familia::Horreum
|
17
|
+
field :name
|
18
|
+
end
|
19
|
+
|
20
|
+
## Test that Autoloadable can be included in feature modules
|
21
|
+
TestAutoloadableFeature.ancestors.include?(Familia::Features::Autoloadable)
|
22
|
+
#=> true
|
23
|
+
|
24
|
+
## Test that Autoloadable extends feature modules with ClassMethods
|
25
|
+
TestAutoloadableFeature.respond_to?(:post_inclusion_autoload)
|
26
|
+
#=> true
|
27
|
+
|
28
|
+
## Test that including autoloadable feature in Horreum class works
|
29
|
+
TestModelForAutoloadable.include(TestAutoloadableFeature)
|
30
|
+
TestModelForAutoloadable.ancestors.include?(TestAutoloadableFeature)
|
31
|
+
#=> true
|
32
|
+
|
33
|
+
## Test that post_inclusion_autoload can be called with test class
|
34
|
+
TestAutoloadableFeature.post_inclusion_autoload(TestModelForAutoloadable, :test_autoloadable_feature, {})
|
35
|
+
"success"
|
36
|
+
#=> "success"
|
37
|
+
|
38
|
+
## Test that feature methods are available on the model
|
39
|
+
@test_instance = TestModelForAutoloadable.new(name: 'test')
|
40
|
+
@test_instance.respond_to?(:test_feature_method)
|
41
|
+
#=> true
|
42
|
+
|
43
|
+
## Test that feature method works
|
44
|
+
@test_instance.test_feature_method
|
45
|
+
#=> "feature_loaded"
|
46
|
+
|
47
|
+
## Test that Autoloadable works with DataType classes (should not crash)
|
48
|
+
class TestDataTypeAutoloadable < Familia::DataType
|
49
|
+
include Familia::Features::Autoloadable
|
50
|
+
end
|
51
|
+
|
52
|
+
TestDataTypeAutoloadable.ancestors.include?(Familia::Features::Autoloadable)
|
53
|
+
#=> true
|
54
|
+
|
55
|
+
## Test that SafeDump includes Autoloadable (real-world usage)
|
56
|
+
Familia::Features::SafeDump.ancestors.include?(Familia::Features::Autoloadable)
|
57
|
+
#=> true
|
58
|
+
|
59
|
+
## Test that SafeDump has post_inclusion_autoload capability
|
60
|
+
Familia::Features::SafeDump.respond_to?(:post_inclusion_autoload)
|
61
|
+
#=> true
|
@@ -77,9 +77,14 @@ end
|
|
77
77
|
@doc.content.to_str
|
78
78
|
#=!> NoMethodError
|
79
79
|
|
80
|
-
## JSON serialization - to_json
|
81
|
-
|
82
|
-
|
80
|
+
## JSON serialization - to_json (fails for security)
|
81
|
+
begin
|
82
|
+
@doc.content.to_json
|
83
|
+
raise "Should have raised SerializerError"
|
84
|
+
rescue Familia::SerializerError => e
|
85
|
+
e.class
|
86
|
+
end
|
87
|
+
#=> Familia::SerializerError
|
83
88
|
|
84
89
|
## JSON serialization - as_json
|
85
90
|
@doc.content.as_json
|
@@ -188,18 +188,30 @@ rescue TypeError => e
|
|
188
188
|
end
|
189
189
|
#=> true
|
190
190
|
|
191
|
-
## JSON serialization
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
191
|
+
## JSON serialization prevents leakage by raising error
|
192
|
+
begin
|
193
|
+
user_json = {
|
194
|
+
id: @user.id,
|
195
|
+
username: @user.username,
|
196
|
+
password: @user.password_hash
|
197
|
+
}.to_json
|
198
|
+
false
|
199
|
+
rescue Familia::SerializerError
|
200
|
+
true
|
201
|
+
end
|
202
|
+
#=> true
|
200
203
|
|
201
|
-
## JSON
|
202
|
-
|
204
|
+
## JSON serialization with ConcealedString raises error
|
205
|
+
begin
|
206
|
+
user_json = {
|
207
|
+
id: @user.id,
|
208
|
+
username: @user.username,
|
209
|
+
password: @user.password_hash
|
210
|
+
}.to_json
|
211
|
+
false
|
212
|
+
rescue Familia::SerializerError => e
|
213
|
+
e.message.include?("ConcealedString")
|
214
|
+
end
|
203
215
|
#=> true
|
204
216
|
|
205
217
|
## Bulk field operations are secure
|
@@ -281,16 +293,46 @@ api_response = {
|
|
281
293
|
}
|
282
294
|
}
|
283
295
|
|
284
|
-
|
285
|
-
@response_json.
|
286
|
-
|
296
|
+
begin
|
297
|
+
@response_json = api_response.to_json
|
298
|
+
false
|
299
|
+
rescue Familia::SerializerError
|
300
|
+
true
|
301
|
+
end
|
302
|
+
#=> true
|
287
303
|
|
288
304
|
## API response doesn't leak secrets
|
289
|
-
|
290
|
-
|
305
|
+
api_response = {
|
306
|
+
user_id: @user.id,
|
307
|
+
credentials: {
|
308
|
+
password: @user.password_hash,
|
309
|
+
api_key: @user.api_secret
|
310
|
+
}
|
311
|
+
}
|
312
|
+
|
313
|
+
begin
|
314
|
+
@response_json = api_response.to_json
|
315
|
+
false
|
316
|
+
rescue Familia::SerializerError
|
317
|
+
true
|
318
|
+
end
|
319
|
+
#=> true
|
291
320
|
|
292
321
|
## API response contains concealed markers
|
293
|
-
|
322
|
+
api_response = {
|
323
|
+
user_id: @user.id,
|
324
|
+
credentials: {
|
325
|
+
password: @user.password_hash,
|
326
|
+
api_key: @user.api_secret
|
327
|
+
}
|
328
|
+
}
|
329
|
+
|
330
|
+
begin
|
331
|
+
@response_json = api_response.to_json
|
332
|
+
false
|
333
|
+
rescue Familia::SerializerError => e
|
334
|
+
e.message.include?("ConcealedString")
|
335
|
+
end
|
294
336
|
#=> true
|
295
337
|
|
296
338
|
## Debug logging safety
|
@@ -67,9 +67,14 @@ hash_result.keys.include?("api_token")
|
|
67
67
|
@record.api_token.inspect
|
68
68
|
#=> "[CONCEALED]"
|
69
69
|
|
70
|
-
## JSON serialization - to_json
|
71
|
-
|
72
|
-
|
70
|
+
## JSON serialization - to_json (fails for security)
|
71
|
+
begin
|
72
|
+
@record.api_token.to_json
|
73
|
+
raise "Should have raised SerializerError"
|
74
|
+
rescue Familia::SerializerError => e
|
75
|
+
e.class
|
76
|
+
end
|
77
|
+
#=> Familia::SerializerError
|
73
78
|
|
74
79
|
## JSON serialization - as_json
|
75
80
|
@record.api_token.as_json
|
@@ -100,12 +105,21 @@ hash_result.keys.include?("api_token")
|
|
100
105
|
}
|
101
106
|
}
|
102
107
|
|
103
|
-
|
104
|
-
@serialized.
|
105
|
-
|
108
|
+
begin
|
109
|
+
@serialized = @nested_data.to_json
|
110
|
+
false
|
111
|
+
rescue Familia::SerializerError
|
112
|
+
true
|
113
|
+
end
|
114
|
+
#=> true
|
106
115
|
|
107
|
-
## Nested JSON
|
108
|
-
|
116
|
+
## Nested JSON with ConcealedString raises error
|
117
|
+
begin
|
118
|
+
@nested_data.to_json
|
119
|
+
false
|
120
|
+
rescue Familia::SerializerError => e
|
121
|
+
e.message.include?("ConcealedString cannot be serialized")
|
122
|
+
end
|
109
123
|
#=> true
|
110
124
|
|
111
125
|
## Array of mixed field types safety
|
@@ -116,11 +130,21 @@ hash_result.keys.include?("api_token")
|
|
116
130
|
@record.secret_notes
|
117
131
|
]
|
118
132
|
|
119
|
-
|
120
|
-
|
133
|
+
begin
|
134
|
+
@mixed_array.to_json
|
135
|
+
false
|
136
|
+
rescue Familia::SerializerError
|
137
|
+
true
|
138
|
+
end
|
139
|
+
#=> true
|
121
140
|
|
122
|
-
## Mixed array
|
123
|
-
|
141
|
+
## Mixed array with ConcealedString raises error
|
142
|
+
begin
|
143
|
+
@mixed_array.to_json
|
144
|
+
false
|
145
|
+
rescue Familia::SerializerError => e
|
146
|
+
e.message.include?("ConcealedString")
|
147
|
+
end
|
124
148
|
#=> true
|
125
149
|
|
126
150
|
## String interpolation safety
|
@@ -198,3 +198,29 @@ ExternalIdTest.extid_lookup[@test_obj.extid]
|
|
198
198
|
|
199
199
|
# Cleanup test objects
|
200
200
|
@test_obj.destroy! rescue nil
|
201
|
+
|
202
|
+
## Test 1: Changing extid value (should work after bug fix)
|
203
|
+
bug_test_obj = ExternalIdTest.new(id: 'bug_test', name: 'Bug Test Object')
|
204
|
+
bug_test_obj.save
|
205
|
+
bug_test_obj.extid = 'new_extid_value'
|
206
|
+
bug_test_obj.extid
|
207
|
+
#=> "new_extid_value"
|
208
|
+
|
209
|
+
## Test 2: find_by_extid with deleted object (should work after bug fix)
|
210
|
+
delete_test_obj = ExternalIdTest.new(id: 'delete_test', name: 'Delete Test')
|
211
|
+
delete_test_obj.save
|
212
|
+
test_extid = delete_test_obj.extid
|
213
|
+
# Delete the object directly from Redis to simulate cleanup scenario
|
214
|
+
ExternalIdTest.dbclient.del(delete_test_obj.dbkey)
|
215
|
+
# Now try to find by extid - this should clean up mapping and return nil
|
216
|
+
ExternalIdTest.find_by_extid(test_extid)
|
217
|
+
#=> nil
|
218
|
+
|
219
|
+
## Test 3: destroy! method (should work after bug fix)
|
220
|
+
destroy_test_obj = ExternalIdTest.new(id: 'destroy_test', name: 'Destroy Test')
|
221
|
+
destroy_test_obj.save
|
222
|
+
destroy_extid = destroy_test_obj.extid
|
223
|
+
destroy_test_obj.destroy!
|
224
|
+
# Verify mapping was cleaned up
|
225
|
+
ExternalIdTest.extid_lookup.key?(destroy_extid)
|
226
|
+
#=> false
|
@@ -46,7 +46,8 @@ class ::TestModelWithSafeDump
|
|
46
46
|
safe_dump_field :id
|
47
47
|
safe_dump_field :name
|
48
48
|
safe_dump_field :status, ->(obj) { obj.active? ? 'active' : 'inactive' }
|
49
|
-
|
49
|
+
safe_dump_field :email
|
50
|
+
safe_dump_field :computed_field, ->(obj) { "#{obj.name}-computed" }
|
50
51
|
end
|
51
52
|
|
52
53
|
# Test field definitions in feature modules
|
@@ -101,7 +101,7 @@ SafeDumpCategoryTest.features_enabled.include?(:safe_dump)
|
|
101
101
|
@safedump_result.keys.sort
|
102
102
|
#=> [:email, :id, :public_name]
|
103
103
|
|
104
|
-
## Safe dump respects
|
104
|
+
## Safe dump respects safe_dump_field configuration
|
105
105
|
@safedump_result.key?(:tryouts_cache_data)
|
106
106
|
#=> false
|
107
107
|
|