active_capture 1.1.0 → 1.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.
- checksums.yaml +4 -4
- data/lib/active_capture.rb +118 -81
- data/lib/support/capture_storage.rb +151 -0
- data/lib/support/version.rb +5 -0
- data/test/support/test_capture_storage.rb +155 -0
- data/test/test_active_capture.rb +25 -3
- metadata +4 -3
- data/lib/active_capture/capture_storage.rb +0 -29
- data/lib/active_capture/version.rb +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 35e9f57cd205fbbb0ed7e4139101787cb4f66e53a32186b27473f9db72b40498
|
4
|
+
data.tar.gz: ea2747f578ebc4dff315d542b7e6e4cb9193e53e0dd49f2231f37654d39047bc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f72ad16293a0f39eed9e2471ba7ece51022abdcb6f6005244f25502f7d903eb5054b983a2414fbbeb18c0b1878a93bf674333c71bcd4e4cea57a37df841cc8bf
|
7
|
+
data.tar.gz: c4d02f5b01eb45a1a2e98cd349b4fd3d765a8a4ff27dfe48a57568ac06594431ba501dd69652292776724f548c8528f92b3cd1a613cff5c0a5f22024d4355868
|
data/lib/active_capture.rb
CHANGED
@@ -1,106 +1,143 @@
|
|
1
|
-
require '
|
2
|
-
require_relative '
|
1
|
+
require 'support/version'
|
2
|
+
require_relative 'support/capture_storage'
|
3
3
|
require 'json'
|
4
4
|
require 'active_record'
|
5
5
|
|
6
6
|
module ActiveCapture
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
CaptureStorage.save_capture(record, capture_data, name)
|
7
|
+
def self.take(record, associations: [], name: nil)
|
8
|
+
validate_record(record)
|
9
|
+
|
10
|
+
capture_data = {
|
11
|
+
model: record.class.name,
|
12
|
+
record_id: record.id,
|
13
|
+
attributes: record.attributes,
|
14
|
+
associations: capture_associations(record, associations)
|
15
|
+
}
|
16
|
+
|
17
|
+
begin
|
18
|
+
Support::CaptureStorage.save_capture(record, capture_data, name)
|
19
|
+
rescue StandardError => e
|
20
|
+
raise "Failed to save capture: #{e.message}"
|
19
21
|
end
|
22
|
+
end
|
20
23
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
raise ArgumentError, 'capture file does not match the given record' unless captured_records['record_id'] == record.id
|
24
|
+
def self.restore(record, capture_file, merge: false)
|
25
|
+
captured_records = Support::CaptureStorage.load_capture(capture_file)
|
26
|
+
validate_capture(record, captured_records)
|
25
27
|
|
26
|
-
|
28
|
+
ActiveRecord::Base.transaction do
|
29
|
+
begin
|
27
30
|
record.update!(captured_records['attributes'])
|
28
31
|
restore_associations(record, captured_records['associations'], merge: merge)
|
32
|
+
rescue StandardError => e
|
33
|
+
raise "Failed to restore record: #{e.message}"
|
29
34
|
end
|
30
35
|
end
|
36
|
+
end
|
31
37
|
|
32
|
-
|
38
|
+
private
|
33
39
|
|
34
|
-
|
35
|
-
|
40
|
+
def self.validate_record(record)
|
41
|
+
unless record.is_a?(ActiveRecord::Base)
|
42
|
+
raise ArgumentError, 'Record must be an ActiveRecord::Base instance'
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.validate_capture(record, captured_records)
|
47
|
+
unless captured_records['record_id'] == record.id
|
48
|
+
raise ArgumentError, 'Capture file does not match the given record'
|
49
|
+
end
|
50
|
+
end
|
36
51
|
|
37
|
-
|
38
|
-
|
52
|
+
def self.capture_associations(record, associations)
|
53
|
+
associations.each_with_object({}) do |association, association_data|
|
54
|
+
association_name, nested_associations = parse_association(association)
|
39
55
|
|
40
|
-
|
41
|
-
association_name = association.keys.first
|
42
|
-
nested_associations = association.values.flatten
|
43
|
-
else
|
44
|
-
association_name = association
|
45
|
-
end
|
56
|
+
next unless record.respond_to?(association_name)
|
46
57
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
attributes: related_records.attributes,
|
53
|
-
associations: capture_associations(related_records, nested_associations)
|
54
|
-
}
|
55
|
-
elsif related_records.respond_to?(:map)
|
56
|
-
association_data[association_name] = related_records.map do |related_record|
|
57
|
-
{
|
58
|
-
attributes: related_record.attributes,
|
59
|
-
associations: capture_associations(related_record, nested_associations)
|
60
|
-
}
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
58
|
+
begin
|
59
|
+
related_records = record.send(association_name)
|
60
|
+
association_data[association_name] = capture_related_records(related_records, nested_associations)
|
61
|
+
rescue StandardError => e
|
62
|
+
raise "Failed to capture association #{association_name}: #{e.message}"
|
64
63
|
end
|
64
|
+
end
|
65
|
+
end
|
65
66
|
|
66
|
-
|
67
|
+
def self.capture_related_records(related_records, nested_associations)
|
68
|
+
if related_records.is_a?(ActiveRecord::Base)
|
69
|
+
{
|
70
|
+
attributes: related_records.attributes,
|
71
|
+
associations: capture_associations(related_records, nested_associations)
|
72
|
+
}
|
73
|
+
elsif related_records.respond_to?(:map)
|
74
|
+
related_records.map do |related_record|
|
75
|
+
{
|
76
|
+
attributes: related_record.attributes,
|
77
|
+
associations: capture_associations(related_record, nested_associations)
|
78
|
+
}
|
79
|
+
end
|
67
80
|
end
|
81
|
+
end
|
68
82
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
else
|
98
|
-
new_record = associated_class.create!(related_data['attributes'])
|
99
|
-
record.update!(association_name => new_record)
|
100
|
-
restore_associations(new_record, related_data['associations'], merge: merge)
|
101
|
-
end
|
83
|
+
def self.parse_association(association)
|
84
|
+
if association.is_a?(Hash)
|
85
|
+
[association.keys.first, association.values.flatten]
|
86
|
+
else
|
87
|
+
[association, []]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.restore_associations(record, associations_data, merge: false)
|
92
|
+
associations_data.each do |association_name, related_data|
|
93
|
+
if related_data.is_a?(Array)
|
94
|
+
restore_collection_association(record, association_name, related_data, merge)
|
95
|
+
else
|
96
|
+
restore_single_association(record, association_name, related_data, merge)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.restore_collection_association(record, association_name, related_data, merge)
|
102
|
+
associated_class = record.class.reflect_on_association(association_name).klass
|
103
|
+
existing_records = record.send(association_name)
|
104
|
+
|
105
|
+
related_data.each do |related_record_data|
|
106
|
+
if merge && related_record_data['attributes']['id']
|
107
|
+
existing_record = existing_records.find_by(id: related_record_data['attributes']['id'])
|
108
|
+
if existing_record
|
109
|
+
update_and_restore(existing_record, related_record_data, merge)
|
110
|
+
next
|
102
111
|
end
|
103
112
|
end
|
113
|
+
create_and_restore(associated_class, related_record_data, record, association_name)
|
104
114
|
end
|
105
115
|
end
|
116
|
+
|
117
|
+
def self.restore_single_association(record, association_name, related_data, merge)
|
118
|
+
associated_class = record.class.reflect_on_association(association_name).klass
|
119
|
+
|
120
|
+
if merge && related_data['attributes']['id']
|
121
|
+
existing_record = record.send(association_name)
|
122
|
+
if existing_record&.id == related_data['attributes']['id']
|
123
|
+
update_and_restore(existing_record, related_data, merge)
|
124
|
+
return
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
new_record = create_and_restore(associated_class, related_data)
|
129
|
+
record.update!(association_name => new_record)
|
130
|
+
end
|
131
|
+
|
132
|
+
def self.update_and_restore(record, related_data, merge)
|
133
|
+
record.update!(related_data['attributes'])
|
134
|
+
restore_associations(record, related_data['associations'], merge: merge)
|
135
|
+
end
|
136
|
+
|
137
|
+
def self.create_and_restore(associated_class, related_data, parent_record = nil, association_name = nil)
|
138
|
+
new_record = associated_class.create!(related_data['attributes'])
|
139
|
+
restore_associations(new_record, related_data['associations'], merge: false)
|
140
|
+
parent_record&.update!(association_name => new_record) if association_name
|
141
|
+
new_record
|
142
|
+
end
|
106
143
|
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
module Support
|
6
|
+
class CaptureStorage
|
7
|
+
CAPTURE_DIR = 'captures'.freeze
|
8
|
+
MAX_FILENAME_LENGTH = 100
|
9
|
+
VALID_FILENAME_REGEX = /\A[a-zA-Z0-9\-_\.]+\z/
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def save_capture(record, capture_data, name = nil)
|
13
|
+
validate_record(record)
|
14
|
+
validate_capture_data(capture_data)
|
15
|
+
|
16
|
+
file_name = generate_filename(record, name)
|
17
|
+
dir_path = create_capture_directory(record)
|
18
|
+
file_path = File.join(dir_path, file_name)
|
19
|
+
|
20
|
+
write_capture_file(file_path, capture_data)
|
21
|
+
|
22
|
+
{ success: true, file_path: file_path }
|
23
|
+
rescue => e
|
24
|
+
{ success: false, error: e.message }
|
25
|
+
end
|
26
|
+
|
27
|
+
def load_capture(capture_file)
|
28
|
+
validate_file_path(capture_file)
|
29
|
+
|
30
|
+
file_content = read_file_safely(capture_file)
|
31
|
+
parsed_data = JSON.parse(file_content)
|
32
|
+
|
33
|
+
validate_parsed_data(parsed_data)
|
34
|
+
|
35
|
+
parsed_data
|
36
|
+
rescue => e
|
37
|
+
raise StorageError, "Failed to load capture: #{e.message}"
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def validate_record(record)
|
43
|
+
unless record.respond_to?(:class) && record.respond_to?(:id)
|
44
|
+
raise ArgumentError, "Invalid record: must respond to :class and :id"
|
45
|
+
end
|
46
|
+
|
47
|
+
if record.id.nil?
|
48
|
+
raise ArgumentError, "Cannot capture unsaved record (ID is nil)"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def validate_capture_data(data)
|
53
|
+
unless data.is_a?(Hash)
|
54
|
+
raise ArgumentError, "Capture data must be a Hash"
|
55
|
+
end
|
56
|
+
|
57
|
+
required_keys = [:model, :record_id, :attributes]
|
58
|
+
missing_keys = required_keys.reject { |k| data.key?(k) }
|
59
|
+
unless missing_keys.empty?
|
60
|
+
raise ArgumentError, "Missing required keys in capture data: #{missing_keys.join(', ')}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def generate_filename(record, custom_name)
|
65
|
+
model_name = record.class.name.downcase.gsub(/[^a-z0-9]/, '_')
|
66
|
+
record_id = record.id.to_s
|
67
|
+
timestamp = Time.now.strftime('%Y%m%d%H%M%S')
|
68
|
+
|
69
|
+
if custom_name
|
70
|
+
validate_filename(custom_name)
|
71
|
+
base_name = custom_name.gsub(/\s+/, '_')[0..MAX_FILENAME_LENGTH]
|
72
|
+
"#{base_name}.json"
|
73
|
+
else
|
74
|
+
"#{model_name}_#{record_id}_#{timestamp}.json"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def validate_filename(name)
|
79
|
+
if name.empty?
|
80
|
+
raise ArgumentError, "Filename cannot be empty"
|
81
|
+
end
|
82
|
+
|
83
|
+
if name.length > MAX_FILENAME_LENGTH
|
84
|
+
raise ArgumentError, "Filename too long (max #{MAX_FILENAME_LENGTH} chars)"
|
85
|
+
end
|
86
|
+
|
87
|
+
unless name.match(VALID_FILENAME_REGEX)
|
88
|
+
raise ArgumentError, "Filename contains invalid characters"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def create_capture_directory(record)
|
93
|
+
model_name = record.class.name.downcase.gsub(/[^a-z0-9]/, '_')
|
94
|
+
dir_path = File.join(CAPTURE_DIR, model_name)
|
95
|
+
|
96
|
+
begin
|
97
|
+
FileUtils.mkdir_p(dir_path)
|
98
|
+
rescue SystemCallError => e
|
99
|
+
raise StorageError, "Failed to create directory '#{dir_path}': #{e.message}"
|
100
|
+
end
|
101
|
+
|
102
|
+
dir_path
|
103
|
+
end
|
104
|
+
|
105
|
+
def write_capture_file(file_path, data)
|
106
|
+
begin
|
107
|
+
temp_path = "#{file_path}.tmp"
|
108
|
+
File.write(temp_path, JSON.pretty_generate(data))
|
109
|
+
File.rename(temp_path, file_path)
|
110
|
+
rescue SystemCallError => e
|
111
|
+
File.delete(temp_path) if File.exist?(temp_path)
|
112
|
+
raise StorageError, "Failed to write capture file '#{file_path}': #{e.message}"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def validate_file_path(file_path)
|
117
|
+
unless File.exist?(file_path)
|
118
|
+
raise StorageError, "File does not exist: #{file_path}"
|
119
|
+
end
|
120
|
+
|
121
|
+
unless File.readable?(file_path)
|
122
|
+
raise StorageError, "No read permission for file: #{file_path}"
|
123
|
+
end
|
124
|
+
|
125
|
+
if File.directory?(file_path)
|
126
|
+
raise StorageError, "Path is a directory, not a file: #{file_path}"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def read_file_safely(file_path)
|
131
|
+
File.read(file_path)
|
132
|
+
rescue SystemCallError => e
|
133
|
+
raise StorageError, "Failed to read file '#{file_path}': #{e.message}"
|
134
|
+
end
|
135
|
+
|
136
|
+
def validate_parsed_data(data)
|
137
|
+
unless data.is_a?(Hash)
|
138
|
+
raise StorageError, "Invalid capture format: expected JSON object"
|
139
|
+
end
|
140
|
+
|
141
|
+
required_keys = ['model', 'record_id', 'attributes']
|
142
|
+
missing_keys = required_keys.reject { |k| data.key?(k) }
|
143
|
+
unless missing_keys.empty?
|
144
|
+
raise StorageError, "Invalid capture data: missing keys #{missing_keys.join(', ')}"
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
class StorageError < StandardError; end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'minitest/autorun'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
# Define our test classes at the top level
|
6
|
+
class TestModel; end
|
7
|
+
class NewModel; end
|
8
|
+
class ProtectedModel; end
|
9
|
+
|
10
|
+
class CaptureStorageTest < Minitest::Test
|
11
|
+
# Simple test record class that mimics ActiveRecord::Base
|
12
|
+
class TestRecord
|
13
|
+
attr_reader :id
|
14
|
+
|
15
|
+
def initialize(id, class_name)
|
16
|
+
@id = id
|
17
|
+
@class_name = class_name
|
18
|
+
end
|
19
|
+
|
20
|
+
def class
|
21
|
+
@class_name.constantize
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def setup
|
26
|
+
# Create test data directory
|
27
|
+
@test_dir = 'test_data'
|
28
|
+
FileUtils.mkdir_p(@test_dir)
|
29
|
+
|
30
|
+
# Create sample capture data
|
31
|
+
@sample_data = {
|
32
|
+
model: 'TestModel',
|
33
|
+
record_id: 1,
|
34
|
+
attributes: { name: 'Test', value: 42 },
|
35
|
+
associations: {
|
36
|
+
comments: [
|
37
|
+
{ attributes: { id: 1, content: 'First comment' } },
|
38
|
+
{ attributes: { id: 2, content: 'Second comment' } }
|
39
|
+
]
|
40
|
+
}
|
41
|
+
}
|
42
|
+
|
43
|
+
# Create test files
|
44
|
+
@valid_file = "#{@test_dir}/valid.json"
|
45
|
+
File.write(@valid_file, JSON.generate(@sample_data))
|
46
|
+
|
47
|
+
@invalid_json_file = "#{@test_dir}/invalid.json"
|
48
|
+
File.write(@invalid_json_file, 'not valid json')
|
49
|
+
|
50
|
+
@empty_file = "#{@test_dir}/empty.json"
|
51
|
+
File.write(@empty_file, '')
|
52
|
+
|
53
|
+
# Clear captures directory before each test
|
54
|
+
FileUtils.rm_rf(Support::CaptureStorage::CAPTURE_DIR) if File.directory?(Support::CaptureStorage::CAPTURE_DIR)
|
55
|
+
end
|
56
|
+
|
57
|
+
def teardown
|
58
|
+
# Clean up test files
|
59
|
+
FileUtils.rm_rf(@test_dir)
|
60
|
+
FileUtils.rm_rf(Support::CaptureStorage::CAPTURE_DIR)
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_save_capture_creates_valid_file
|
64
|
+
test_record = TestRecord.new(1, 'TestModel')
|
65
|
+
result = Support::CaptureStorage.save_capture(test_record, @sample_data)
|
66
|
+
|
67
|
+
assert result[:success], "Save should succeed: #{result[:error]}"
|
68
|
+
assert File.exist?(result[:file_path]), "File should exist at #{result[:file_path]}"
|
69
|
+
|
70
|
+
file_content = JSON.parse(File.read(result[:file_path]))
|
71
|
+
assert_equal 'TestModel', file_content['model']
|
72
|
+
assert_equal 1, file_content['record_id']
|
73
|
+
end
|
74
|
+
|
75
|
+
def test_save_capture_with_custom_name
|
76
|
+
test_record = TestRecord.new(1, 'TestModel')
|
77
|
+
custom_name = 'custom_capture'
|
78
|
+
result = Support::CaptureStorage.save_capture(test_record, @sample_data, custom_name)
|
79
|
+
|
80
|
+
assert result[:success], "Save should succeed: #{result[:error]}"
|
81
|
+
assert_match /#{custom_name}\.json/, result[:file_path]
|
82
|
+
assert File.exist?(result[:file_path]), "File should exist at #{result[:file_path]}"
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_save_capture_invalid_filename
|
86
|
+
test_record = TestRecord.new(1, 'TestModel')
|
87
|
+
result = Support::CaptureStorage.save_capture(test_record, @sample_data, 'invalid/name')
|
88
|
+
|
89
|
+
refute result[:success], "Save should fail with invalid filename"
|
90
|
+
assert_match /Filename contains invalid characters/, result[:error], "Error should mention invalid filename"
|
91
|
+
end
|
92
|
+
|
93
|
+
def test_load_capture_valid_file
|
94
|
+
data = Support::CaptureStorage.load_capture(@valid_file)
|
95
|
+
|
96
|
+
assert_equal 'TestModel', data['model']
|
97
|
+
assert_equal 1, data['record_id']
|
98
|
+
assert_equal 2, data['associations']['comments'].size
|
99
|
+
end
|
100
|
+
|
101
|
+
def test_load_capture_nonexistent_file
|
102
|
+
assert_raises Support::CaptureStorage::StorageError do
|
103
|
+
Support::CaptureStorage.load_capture('nonexistent.json')
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def test_load_capture_invalid_json
|
108
|
+
assert_raises Support::CaptureStorage::StorageError do
|
109
|
+
Support::CaptureStorage.load_capture(@invalid_json_file)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def test_load_capture_empty_file
|
114
|
+
assert_raises Support::CaptureStorage::StorageError do
|
115
|
+
Support::CaptureStorage.load_capture(@empty_file)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def test_load_capture_missing_required_keys
|
120
|
+
incomplete_data = { 'some_key' => 'some_value' }
|
121
|
+
incomplete_file = "#{@test_dir}/incomplete.json"
|
122
|
+
File.write(incomplete_file, JSON.generate(incomplete_data))
|
123
|
+
|
124
|
+
assert_raises Support::CaptureStorage::StorageError do
|
125
|
+
Support::CaptureStorage.load_capture(incomplete_file)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def test_atomic_write_handles_failure
|
130
|
+
# Create a directory we can't write to
|
131
|
+
protected_dir = "#{@test_dir}/protected"
|
132
|
+
FileUtils.mkdir_p(protected_dir)
|
133
|
+
FileUtils.chmod(0444, protected_dir) # read-only
|
134
|
+
|
135
|
+
test_record = TestRecord.new(1, 'ProtectedModel')
|
136
|
+
file_path = "#{protected_dir}/test.json"
|
137
|
+
|
138
|
+
assert_raises Support::CaptureStorage::StorageError do
|
139
|
+
Support::CaptureStorage.send(:write_capture_file, file_path, @sample_data)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Verify no temp file remains
|
143
|
+
refute File.exist?("#{file_path}.tmp")
|
144
|
+
ensure
|
145
|
+
FileUtils.chmod(0755, protected_dir) if File.exist?(protected_dir)
|
146
|
+
end
|
147
|
+
|
148
|
+
def test_directory_creation
|
149
|
+
test_record = TestRecord.new(1, 'NewModel')
|
150
|
+
result = Support::CaptureStorage.save_capture(test_record, @sample_data)
|
151
|
+
|
152
|
+
assert result[:success], "Save should succeed: #{result[:error]}"
|
153
|
+
assert File.directory?('captures/newmodel'), "Directory should be created"
|
154
|
+
end
|
155
|
+
end
|
data/test/test_active_capture.rb
CHANGED
@@ -2,6 +2,7 @@ require 'test_helper'
|
|
2
2
|
require 'minitest/autorun'
|
3
3
|
|
4
4
|
class ActiveCaptureTest < Minitest::Test
|
5
|
+
# Setup method to initialize test data before each test
|
5
6
|
def setup
|
6
7
|
@user = User.create!(name: "John Doe", email: "john@example.com")
|
7
8
|
@post = @user.posts.create!(title: "First Post", content: "This is my first post.")
|
@@ -9,29 +10,50 @@ class ActiveCaptureTest < Minitest::Test
|
|
9
10
|
@comment2 = @post.comments.create!(content: "Thanks for sharing.")
|
10
11
|
end
|
11
12
|
|
13
|
+
# Teardown method to clean up after each test
|
14
|
+
def teardown
|
15
|
+
FileUtils.rm_rf('captures/')
|
16
|
+
end
|
17
|
+
|
18
|
+
# Test capturing a user with associated posts and comments
|
12
19
|
def test_capture_with_associations
|
13
|
-
|
20
|
+
# Take a capture of the user and their associations
|
21
|
+
ActiveCapture.take(@user, associations: [:posts, { posts: :comments }])
|
14
22
|
|
23
|
+
# Verify that capture files are created
|
15
24
|
capture_files = Dir["captures/user/*.json"]
|
16
25
|
assert !capture_files.empty?, "capture file should be created"
|
17
26
|
|
27
|
+
# Verify the content of the capture file
|
18
28
|
capture_content = JSON.parse(File.read(capture_files.last))
|
19
29
|
assert_equal 1, capture_content["associations"]["posts"].size
|
20
30
|
assert_equal 2, capture_content["associations"]["posts"].first["associations"]["comments"].size
|
21
31
|
end
|
22
32
|
|
33
|
+
# Test restoring a user with merge functionality
|
23
34
|
def test_restore_with_merge
|
24
|
-
|
35
|
+
# Take a capture of the user and their associations
|
36
|
+
ActiveCapture.take(@user, associations: [:posts, { posts: :comments }])
|
25
37
|
capture_file = Dir["captures/user/*.json"].last
|
38
|
+
|
39
|
+
# Modify the post and add a new comment
|
26
40
|
@post.update(title: "updated title")
|
27
41
|
new_comment = @post.comments.create!(content: "Another comment")
|
28
|
-
ActiveCapture::Capture.restore(@user, capture_file, merge: true)
|
29
42
|
|
43
|
+
# Restore the user from the capture file with merge enabled
|
44
|
+
ActiveCapture.restore(@user, capture_file, merge: true)
|
45
|
+
|
46
|
+
# Reload the user and post to verify changes
|
30
47
|
@user.reload
|
31
48
|
@post.reload
|
32
49
|
|
50
|
+
# Verify that the user's name is restored
|
33
51
|
assert_equal "John Doe", @user.name
|
52
|
+
|
53
|
+
# Verify that the post's title is restored
|
34
54
|
assert_equal "First Post", @post.title
|
55
|
+
|
56
|
+
# Verify that the new comment still exists after the merge
|
35
57
|
assert_equal 3, @post.comments.count
|
36
58
|
assert @post.comments.exists?(new_comment.id), "New comment should still exist after merge"
|
37
59
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_capture
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1.
|
4
|
+
version: 1.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tanmay Bhawsar
|
@@ -32,8 +32,9 @@ extensions: []
|
|
32
32
|
extra_rdoc_files: []
|
33
33
|
files:
|
34
34
|
- lib/active_capture.rb
|
35
|
-
- lib/
|
36
|
-
- lib/
|
35
|
+
- lib/support/capture_storage.rb
|
36
|
+
- lib/support/version.rb
|
37
|
+
- test/support/test_capture_storage.rb
|
37
38
|
- test/test_active_capture.rb
|
38
39
|
- test/test_helper.rb
|
39
40
|
homepage: https://github.com/bhawsartanmay/active_capture
|
@@ -1,29 +0,0 @@
|
|
1
|
-
require 'json'
|
2
|
-
require 'fileutils'
|
3
|
-
|
4
|
-
module ActiveCapture
|
5
|
-
class CaptureStorage
|
6
|
-
CAPTURE_DIR = 'captures'
|
7
|
-
|
8
|
-
def self.save_capture(record, capture_data, name = nil)
|
9
|
-
model_name = record.class.name.downcase
|
10
|
-
record_id = record.id
|
11
|
-
timestamp = Time.now.strftime('%Y%m%d%H%M%S')
|
12
|
-
file_name = name ? "#{name}.json" : "#{record_id}_#{timestamp}.json"
|
13
|
-
|
14
|
-
dir_path = File.join(CAPTURE_DIR, model_name)
|
15
|
-
FileUtils.mkdir_p(dir_path)
|
16
|
-
|
17
|
-
file_path = File.join(dir_path, file_name)
|
18
|
-
File.write(file_path, JSON.pretty_generate(capture_data))
|
19
|
-
|
20
|
-
puts "Capture saved to #{file_path}"
|
21
|
-
end
|
22
|
-
|
23
|
-
def self.load_capture(capture_file)
|
24
|
-
raise ArgumentError, "Capture file does not exist #{capture_file}" unless File.exist?(capture_file)
|
25
|
-
|
26
|
-
JSON.parse(File.read(capture_file))
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|