familia 2.0.0.pre10 → 2.0.0.pre13
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 +507 -0
- data/CLAUDE.md +5 -55
- data/Gemfile +1 -6
- data/Gemfile.lock +13 -7
- data/changelog.d/README.md +45 -34
- data/changelog.d/scriv.ini +5 -0
- data/docs/archive/FAMILIA_RELATIONSHIPS.md +1 -1
- data/docs/archive/FAMILIA_UPDATE.md +1 -1
- data/docs/archive/README.md +15 -19
- data/docs/guides/Feature-System-Autoloading.md +228 -0
- data/docs/guides/Home.md +1 -1
- data/docs/guides/Implementation-Guide.md +1 -1
- data/docs/guides/relationships-methods.md +1 -1
- data/docs/guides/time-utilities.md +221 -0
- data/docs/migrating/.gitignore +2 -0
- data/docs/migrating/v2.0.0-pre.md +84 -0
- data/docs/migrating/v2.0.0-pre11.md +253 -0
- data/docs/migrating/v2.0.0-pre12.md +306 -0
- data/docs/migrating/v2.0.0-pre13.md +329 -0
- data/docs/migrating/v2.0.0-pre5.md +110 -0
- data/docs/migrating/v2.0.0-pre6.md +154 -0
- data/docs/migrating/v2.0.0-pre7.md +222 -0
- data/docs/overview.md +6 -7
- data/{examples/redis_command_validation_example.rb → docs/reference/auditing_database_commands.rb} +29 -32
- data/examples/autoloader/mega_customer/safe_dump_fields.rb +6 -0
- data/examples/autoloader/mega_customer.rb +17 -0
- data/examples/{bit_encoding_integration.rb → permissions.rb} +30 -27
- data/examples/{relationships_basic.rb → relationships.rb} +2 -3
- data/examples/safe_dump.rb +281 -0
- data/familia.gemspec +5 -4
- data/lib/familia/autoloader.rb +53 -0
- data/lib/familia/base.rb +57 -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_request_cache.rb → encryption/request_cache.rb} +1 -1
- data/lib/familia/encryption.rb +1 -1
- data/lib/familia/errors.rb +5 -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 +310 -0
- data/lib/familia/features/object_identifier.rb +307 -0
- data/lib/familia/features/quantization.rb +5 -0
- data/lib/familia/features/safe_dump.rb +74 -73
- data/lib/familia/features.rb +109 -17
- data/lib/familia/field_type.rb +2 -0
- data/lib/familia/horreum/core/serialization.rb +3 -3
- data/lib/familia/horreum/subclass/definition.rb +50 -7
- 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_utils.rb +248 -0
- data/lib/familia/refinements.rb +3 -49
- data/lib/familia/secure_identifier.rb +51 -75
- 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/verifiable_identifier.rb +162 -0
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +15 -2
- 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/secure_identifier_try.rb +47 -18
- data/try/core/time_utils_try.rb +130 -0
- data/try/core/verifiable_identifier_try.rb +171 -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_identifiers/external_identifiers_try.rb → external_identifier/external_identifier_try.rb} +25 -28
- data/try/features/feature_improvements_try.rb +127 -0
- data/try/features/{object_identifiers/object_identifiers_integration_try.rb → object_identifier/object_identifier_integration_try.rb} +28 -30
- data/try/features/{object_identifiers/object_identifiers_try.rb → object_identifier/object_identifier_try.rb} +13 -13
- data/try/features/real_feature_integration_try.rb +8 -7
- data/try/features/safe_dump/safe_dump_autoloading_try.rb +111 -0
- data/try/features/safe_dump/safe_dump_try.rb +8 -9
- data/try/helpers/test_helpers.rb +41 -17
- data/try/integration/cross_component_try.rb +3 -1
- metadata +61 -26
- data/CHANGELOG.md +0 -184
- data/changelog.d/fragments/.keep +0 -0
- data/changelog.d/template.md.j2 +0 -29
- data/lib/familia/core_ext.rb +0 -135
- data/lib/familia/features/external_identifiers/external_identifier_field_type.rb +0 -120
- data/lib/familia/features/external_identifiers.rb +0 -111
- data/lib/familia/features/object_identifiers/object_identifier_field_type.rb +0 -91
- data/lib/familia/features/object_identifiers.rb +0 -194
- data/setup.cfg +0 -12
@@ -56,22 +56,6 @@ hex_trace_id = Familia.generate_hex_trace_id
|
|
56
56
|
[hex_trace_id.class, hex_trace_id.length == 16, hex_trace_id.match?(/^[a-f0-9]+$/)]
|
57
57
|
#=> [String, true, true]
|
58
58
|
|
59
|
-
## Familia.shorten_to_external_id
|
60
|
-
Familia.respond_to?(:shorten_to_external_id)
|
61
|
-
#=> true
|
62
|
-
|
63
|
-
## Can shorten hex ID to external ID (128 bits)
|
64
|
-
hex_id = Familia.generate_hex_id
|
65
|
-
external_id = Familia.shorten_to_external_id(hex_id)
|
66
|
-
[external_id.class, external_id.length < hex_id.length]
|
67
|
-
#=> [String, true]
|
68
|
-
|
69
|
-
## Can shorten hex ID to external ID with custom base (hex)
|
70
|
-
hex_id = Familia.generate_hex_id
|
71
|
-
hex_external_id = Familia.shorten_to_external_id(hex_id, base: 16)
|
72
|
-
[hex_external_id.class, hex_external_id.length == 32]
|
73
|
-
#=> [String, true]
|
74
|
-
|
75
59
|
## Familia.shorten_to_trace_id
|
76
60
|
Familia.respond_to?(:shorten_to_trace_id)
|
77
61
|
#=> true
|
@@ -88,10 +72,55 @@ hex_trace_id = Familia.shorten_to_trace_id(hex_id, base: 16)
|
|
88
72
|
[hex_trace_id.class, hex_trace_id.length == 16]
|
89
73
|
#=> [String, true]
|
90
74
|
|
75
|
+
## Familia.truncate_hex
|
76
|
+
Familia.respond_to?(:truncate_hex)
|
77
|
+
#=> true
|
78
|
+
|
79
|
+
## Can truncate hex ID to 128 bits by default
|
80
|
+
hex_id = Familia.generate_hex_id
|
81
|
+
truncated_id = Familia.truncate_hex(hex_id)
|
82
|
+
[truncated_id.class, truncated_id.length < hex_id.length]
|
83
|
+
#=> [String, true]
|
84
|
+
|
85
|
+
## Can truncate hex ID to a custom bit length (64 bits)
|
86
|
+
hex_id = Familia.generate_hex_id
|
87
|
+
truncated_64 = Familia.truncate_hex(hex_id, bits: 64)
|
88
|
+
[truncated_64.class, truncated_64.length < hex_id.length]
|
89
|
+
#=> [String, true]
|
90
|
+
|
91
|
+
## Can truncate with a custom base (hex)
|
92
|
+
hex_id = Familia.generate_hex_id
|
93
|
+
hex_truncated = Familia.truncate_hex(hex_id, bits: 128, base: 16)
|
94
|
+
[hex_truncated.class, hex_truncated.length == 32]
|
95
|
+
#=> [String, true]
|
96
|
+
|
97
|
+
## Truncated IDs are deterministic
|
98
|
+
hex_id = Familia.generate_hex_id
|
99
|
+
id1 = Familia.truncate_hex(hex_id)
|
100
|
+
id2 = Familia.truncate_hex(hex_id)
|
101
|
+
id1 == id2
|
102
|
+
#=> true
|
103
|
+
|
104
|
+
## Raises error for invalid hex
|
105
|
+
begin
|
106
|
+
Familia.truncate_hex("not-a-hex-string")
|
107
|
+
rescue ArgumentError => e
|
108
|
+
e.message
|
109
|
+
end
|
110
|
+
#=> "Invalid hexadecimal string: not-a-hex-string"
|
111
|
+
|
112
|
+
## Raises error if input bits are less than output bits
|
113
|
+
begin
|
114
|
+
Familia.truncate_hex("abc", bits: 64)
|
115
|
+
rescue ArgumentError => e
|
116
|
+
e.message
|
117
|
+
end
|
118
|
+
#=> "Input bits (12) cannot be less than desired output bits (64)."
|
119
|
+
|
91
120
|
## Shortened IDs are deterministic
|
92
121
|
hex_id = Familia.generate_hex_id
|
93
|
-
id1 = Familia.
|
94
|
-
id2 = Familia.
|
122
|
+
id1 = Familia.shorten_to_trace_id(hex_id)
|
123
|
+
id2 = Familia.shorten_to_trace_id(hex_id)
|
95
124
|
id1 == id2
|
96
125
|
#=> true
|
97
126
|
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require_relative '../helpers/test_helpers'
|
2
|
+
|
3
|
+
module RefinedContext
|
4
|
+
using Familia::Refinements::TimeUtils
|
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 TimeUtils 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::TimeUtils::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::TimeUtils::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::TimeUtils::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::TimeUtils::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::TimeUtils::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::TimeUtils::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::TimeUtils::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::TimeUtils::PER_MONTH) # 2 months ago
|
92
|
+
from_time = Time.now - Familia::Refinements::TimeUtils::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::TimeUtils::PER_YEAR) # 2 years ago
|
99
|
+
from_time = Time.now - Familia::Refinements::TimeUtils::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::TimeUtils::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::TimeUtils::PER_YEAR.round(0)
|
112
|
+
#=> 31556952.0
|
113
|
+
|
114
|
+
## UNIT_METHODS contains months mapping
|
115
|
+
Familia::Refinements::TimeUtils::UNIT_METHODS['months']
|
116
|
+
#=> :months
|
117
|
+
|
118
|
+
## UNIT_METHODS contains mo mapping
|
119
|
+
Familia::Refinements::TimeUtils::UNIT_METHODS['mo']
|
120
|
+
#=> :months
|
121
|
+
|
122
|
+
## UNIT_METHODS contains month mapping
|
123
|
+
Familia::Refinements::TimeUtils::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
|
@@ -0,0 +1,171 @@
|
|
1
|
+
# try/core/verifiable_identifier_try.rb
|
2
|
+
|
3
|
+
require_relative '../helpers/test_helpers'
|
4
|
+
require 'familia/verifiable_identifier'
|
5
|
+
|
6
|
+
## Module is available
|
7
|
+
defined?(Familia::VerifiableIdentifier)
|
8
|
+
#=> "constant"
|
9
|
+
|
10
|
+
## Uses the development secret key by default when ENV is not set
|
11
|
+
Familia::VerifiableIdentifier::SECRET_KEY
|
12
|
+
#=> "cafef00dcafef00dcafef00dcafef00dcafef00dcafef00d"
|
13
|
+
|
14
|
+
# --- Verifiable ID Generation and Verification ---
|
15
|
+
|
16
|
+
## Generates a non-empty string ID
|
17
|
+
id = Familia::VerifiableIdentifier.generate_verifiable_id
|
18
|
+
id.is_a?(String) && !id.empty?
|
19
|
+
#=> true
|
20
|
+
|
21
|
+
## Generated ID is URL-safe (base36)
|
22
|
+
id = Familia::VerifiableIdentifier.generate_verifiable_id
|
23
|
+
id.match?(/[a-z0-9]+/ix)
|
24
|
+
#=> true
|
25
|
+
|
26
|
+
## Generates unique identifiers on subsequent calls
|
27
|
+
Familia::VerifiableIdentifier.generate_verifiable_id
|
28
|
+
#=/> Familia::VerifiableIdentifier.generate_verifiable_id
|
29
|
+
|
30
|
+
## A genuinely generated ID successfully verifies
|
31
|
+
id = Familia::VerifiableIdentifier.generate_verifiable_id
|
32
|
+
Familia::VerifiableIdentifier.verified_identifier?(id)
|
33
|
+
#=> true
|
34
|
+
|
35
|
+
## Fails verification for a completely random garbage string
|
36
|
+
Familia::VerifiableIdentifier.verified_identifier?('this-is-not-a-valid-id-at-all')
|
37
|
+
#=> false
|
38
|
+
|
39
|
+
## Fails verification for a string with invalid characters for the base
|
40
|
+
# A plus sign is not a valid base-36 character.
|
41
|
+
Familia::VerifiableIdentifier.verified_identifier?('this+is+invalid', 36)
|
42
|
+
#=> false
|
43
|
+
|
44
|
+
## Fails verification if the random part of the ID is tampered with
|
45
|
+
id = Familia::VerifiableIdentifier.generate_verifiable_id
|
46
|
+
tampered_id = id.dup
|
47
|
+
tampered_id[0] = (tampered_id[0] == 'a' ? 'b' : 'a') # Flip the first character
|
48
|
+
Familia::VerifiableIdentifier.verified_identifier?(tampered_id)
|
49
|
+
#=> false
|
50
|
+
|
51
|
+
## Fails verification if the tag part of the ID is tampered with
|
52
|
+
id = Familia::VerifiableIdentifier.generate_verifiable_id
|
53
|
+
tampered_id = id.dup
|
54
|
+
idx = tampered_id.length - 1
|
55
|
+
tampered_id[idx] = (tampered_id[idx] == 'a' ? 'b' : 'a') # Flip the last character
|
56
|
+
Familia::VerifiableIdentifier.verified_identifier?(tampered_id)
|
57
|
+
#=> false
|
58
|
+
|
59
|
+
## Works correctly with a different base (hexadecimal)
|
60
|
+
id_hex = Familia::VerifiableIdentifier.generate_verifiable_id(16)
|
61
|
+
Familia::VerifiableIdentifier.verified_identifier?(id_hex, 16)
|
62
|
+
#=> true
|
63
|
+
|
64
|
+
## Base 16 ID has the correct hex length (64 random + 16 tag = 80 chars)
|
65
|
+
id_hex = Familia::VerifiableIdentifier.generate_verifiable_id(16)
|
66
|
+
id_hex.length
|
67
|
+
#=> 80
|
68
|
+
|
69
|
+
# --- Plausibility Checks ---
|
70
|
+
|
71
|
+
## A genuinely generated ID is plausible
|
72
|
+
id = Familia::VerifiableIdentifier.generate_verifiable_id
|
73
|
+
Familia::VerifiableIdentifier.plausible_identifier?(id)
|
74
|
+
#=> true
|
75
|
+
|
76
|
+
## A well-formed but fake ID is still plausible
|
77
|
+
# A string of the correct length (62 for base 36) and charset is plausible
|
78
|
+
total_bits = (Familia::VerifiableIdentifier::RANDOM_HEX_LENGTH + Familia::VerifiableIdentifier::TAG_HEX_LENGTH) * 4
|
79
|
+
fake_id = 'a' * Familia::SecureIdentifier.min_length_for_bits(total_bits, 36)
|
80
|
+
Familia::VerifiableIdentifier.plausible_identifier?(fake_id)
|
81
|
+
#=> true
|
82
|
+
|
83
|
+
## Fails plausibility check if too short
|
84
|
+
short_id = 'a' * 60
|
85
|
+
Familia::VerifiableIdentifier.plausible_identifier?(short_id)
|
86
|
+
#=> false
|
87
|
+
|
88
|
+
## Fails plausibility check if too long
|
89
|
+
long_id = 'a' * 66
|
90
|
+
Familia::VerifiableIdentifier.plausible_identifier?(long_id)
|
91
|
+
#=> false
|
92
|
+
|
93
|
+
## Fails plausibility check for invalid characters
|
94
|
+
invalid_char_id = 'a' * 61 + '+'
|
95
|
+
Familia::VerifiableIdentifier.plausible_identifier?(invalid_char_id)
|
96
|
+
#=> false
|
97
|
+
|
98
|
+
## Fails plausibility check for nil input
|
99
|
+
Familia::VerifiableIdentifier.plausible_identifier?(nil)
|
100
|
+
#=> false
|
101
|
+
|
102
|
+
# --- Scoped Identifier Generation and Verification ---
|
103
|
+
|
104
|
+
## Scoped identifier generation produces different results than unscoped
|
105
|
+
scoped_id = Familia::VerifiableIdentifier.generate_verifiable_id(scope: "example.com")
|
106
|
+
unscoped_id = Familia::VerifiableIdentifier.generate_verifiable_id
|
107
|
+
scoped_id != unscoped_id
|
108
|
+
#=> true
|
109
|
+
|
110
|
+
## Scoped identifiers verify successfully with correct scope
|
111
|
+
scoped_id = Familia::VerifiableIdentifier.generate_verifiable_id(scope: "example.com")
|
112
|
+
Familia::VerifiableIdentifier.verified_identifier?(scoped_id, scope: "example.com")
|
113
|
+
#=> true
|
114
|
+
|
115
|
+
## Scoped identifiers fail verification with wrong scope
|
116
|
+
scoped_id = Familia::VerifiableIdentifier.generate_verifiable_id(scope: "example.com")
|
117
|
+
Familia::VerifiableIdentifier.verified_identifier?(scoped_id, scope: "different.com")
|
118
|
+
#=> false
|
119
|
+
|
120
|
+
## Scoped identifiers fail verification without scope parameter
|
121
|
+
scoped_id = Familia::VerifiableIdentifier.generate_verifiable_id(scope: "example.com")
|
122
|
+
Familia::VerifiableIdentifier.verified_identifier?(scoped_id)
|
123
|
+
#=> false
|
124
|
+
|
125
|
+
## Unscoped identifiers fail verification with scope parameter
|
126
|
+
unscoped_id = Familia::VerifiableIdentifier.generate_verifiable_id
|
127
|
+
Familia::VerifiableIdentifier.verified_identifier?(unscoped_id, scope: "example.com")
|
128
|
+
#=> false
|
129
|
+
|
130
|
+
## Empty string scope produces different identifier than nil scope
|
131
|
+
id_nil = Familia::VerifiableIdentifier.generate_verifiable_id(scope: nil)
|
132
|
+
id_empty = Familia::VerifiableIdentifier.generate_verifiable_id(scope: "")
|
133
|
+
id_nil != id_empty
|
134
|
+
#=> true
|
135
|
+
|
136
|
+
## Empty string scope verifies correctly
|
137
|
+
id_empty = Familia::VerifiableIdentifier.generate_verifiable_id(scope: "")
|
138
|
+
Familia::VerifiableIdentifier.verified_identifier?(id_empty, scope: "")
|
139
|
+
#=> true
|
140
|
+
|
141
|
+
## Short scope values work correctly
|
142
|
+
id_short = Familia::VerifiableIdentifier.generate_verifiable_id(scope: "a")
|
143
|
+
Familia::VerifiableIdentifier.verified_identifier?(id_short, scope: "a")
|
144
|
+
#=> true
|
145
|
+
|
146
|
+
## Long scope values work correctly
|
147
|
+
long_scope = "x" * 1000
|
148
|
+
id_long = Familia::VerifiableIdentifier.generate_verifiable_id(scope: long_scope)
|
149
|
+
Familia::VerifiableIdentifier.verified_identifier?(id_long, scope: long_scope)
|
150
|
+
#=> true
|
151
|
+
|
152
|
+
## Unicode scope values work correctly
|
153
|
+
unicode_scope = "测试🔒🔑"
|
154
|
+
id_unicode = Familia::VerifiableIdentifier.generate_verifiable_id(scope: unicode_scope)
|
155
|
+
Familia::VerifiableIdentifier.verified_identifier?(id_unicode, scope: unicode_scope)
|
156
|
+
#=> true
|
157
|
+
|
158
|
+
## Scoped identifiers work with different bases
|
159
|
+
id_hex = Familia::VerifiableIdentifier.generate_verifiable_id(scope: "test", base: 16)
|
160
|
+
Familia::VerifiableIdentifier.verified_identifier?(id_hex, scope: "test", base: 16)
|
161
|
+
#=> true
|
162
|
+
|
163
|
+
## Backward compatibility: existing method signatures still work
|
164
|
+
id = Familia::VerifiableIdentifier.generate_verifiable_id(36)
|
165
|
+
Familia::VerifiableIdentifier.verified_identifier?(id, 36)
|
166
|
+
#=> true
|
167
|
+
|
168
|
+
## Mixed parameter styles work correctly
|
169
|
+
id = Familia::VerifiableIdentifier.generate_verifiable_id(scope: "test", base: 16)
|
170
|
+
Familia::VerifiableIdentifier.verified_identifier?(id, scope: "test", base: 16)
|
171
|
+
#=> 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
|