active_capture 1.1.0 → 1.1.2
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 +122 -81
- data/lib/support/capture_storage.rb +211 -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
- data/test/test_active_capture_flush.rb +90 -0
- metadata +5 -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: 93b2a116c7a013ba34b42a5211179f14185d2e60d324ff74da5f4f23a8f30a7d
|
4
|
+
data.tar.gz: dd339d97072f5440c0ef2551c73aa6708bc9e22f894fda9e5641eb463d013399
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 17dde4d63324dc3f3e8934e8e5310b3484be942e653068ff9e6d97474d52641571749a556f40985164b9067f3669ed1979f2ccf692479dd998e59c380c549ec9
|
7
|
+
data.tar.gz: 2f0111ff053df52edec0f1632716e45224f1447a811253543212766d685a9d881e7d2b0ec6841cee696bf99ff05c570beff835bfb4daf4674f1a5db3cd493fb5
|
data/lib/active_capture.rb
CHANGED
@@ -1,106 +1,147 @@
|
|
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
|
+
def self.flush(directory)
|
39
|
+
Support::CaptureStorage.flush(directory)
|
40
|
+
end
|
33
41
|
|
34
|
-
|
35
|
-
association_data = {}
|
42
|
+
private
|
36
43
|
|
37
|
-
|
38
|
-
|
44
|
+
def self.validate_record(record)
|
45
|
+
unless record.is_a?(ActiveRecord::Base)
|
46
|
+
raise ArgumentError, 'Record must be an ActiveRecord::Base instance'
|
47
|
+
end
|
48
|
+
end
|
39
49
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
end
|
50
|
+
def self.validate_capture(record, captured_records)
|
51
|
+
unless captured_records['record_id'] == record.id
|
52
|
+
raise ArgumentError, 'Capture file does not match the given record'
|
53
|
+
end
|
54
|
+
end
|
46
55
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
attributes: related_record.attributes,
|
59
|
-
associations: capture_associations(related_record, nested_associations)
|
60
|
-
}
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
56
|
+
def self.capture_associations(record, associations)
|
57
|
+
associations.each_with_object({}) do |association, association_data|
|
58
|
+
association_name, nested_associations = parse_association(association)
|
59
|
+
|
60
|
+
next unless record.respond_to?(association_name)
|
61
|
+
|
62
|
+
begin
|
63
|
+
related_records = record.send(association_name)
|
64
|
+
association_data[association_name] = capture_related_records(related_records, nested_associations)
|
65
|
+
rescue StandardError => e
|
66
|
+
raise "Failed to capture association #{association_name}: #{e.message}"
|
64
67
|
end
|
68
|
+
end
|
69
|
+
end
|
65
70
|
|
66
|
-
|
71
|
+
def self.capture_related_records(related_records, nested_associations)
|
72
|
+
if related_records.is_a?(ActiveRecord::Base)
|
73
|
+
{
|
74
|
+
attributes: related_records.attributes,
|
75
|
+
associations: capture_associations(related_records, nested_associations)
|
76
|
+
}
|
77
|
+
elsif related_records.respond_to?(:map)
|
78
|
+
related_records.map do |related_record|
|
79
|
+
{
|
80
|
+
attributes: related_record.attributes,
|
81
|
+
associations: capture_associations(related_record, nested_associations)
|
82
|
+
}
|
83
|
+
end
|
67
84
|
end
|
85
|
+
end
|
68
86
|
|
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
|
87
|
+
def self.parse_association(association)
|
88
|
+
if association.is_a?(Hash)
|
89
|
+
[association.keys.first, association.values.flatten]
|
90
|
+
else
|
91
|
+
[association, []]
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.restore_associations(record, associations_data, merge: false)
|
96
|
+
associations_data.each do |association_name, related_data|
|
97
|
+
if related_data.is_a?(Array)
|
98
|
+
restore_collection_association(record, association_name, related_data, merge)
|
99
|
+
else
|
100
|
+
restore_single_association(record, association_name, related_data, merge)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def self.restore_collection_association(record, association_name, related_data, merge)
|
106
|
+
associated_class = record.class.reflect_on_association(association_name).klass
|
107
|
+
existing_records = record.send(association_name)
|
108
|
+
|
109
|
+
related_data.each do |related_record_data|
|
110
|
+
if merge && related_record_data['attributes']['id']
|
111
|
+
existing_record = existing_records.find_by(id: related_record_data['attributes']['id'])
|
112
|
+
if existing_record
|
113
|
+
update_and_restore(existing_record, related_record_data, merge)
|
114
|
+
next
|
102
115
|
end
|
103
116
|
end
|
117
|
+
create_and_restore(associated_class, related_record_data, record, association_name)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def self.restore_single_association(record, association_name, related_data, merge)
|
122
|
+
associated_class = record.class.reflect_on_association(association_name).klass
|
123
|
+
|
124
|
+
if merge && related_data['attributes']['id']
|
125
|
+
existing_record = record.send(association_name)
|
126
|
+
if existing_record&.id == related_data['attributes']['id']
|
127
|
+
update_and_restore(existing_record, related_data, merge)
|
128
|
+
return
|
129
|
+
end
|
104
130
|
end
|
131
|
+
|
132
|
+
new_record = create_and_restore(associated_class, related_data)
|
133
|
+
record.update!(association_name => new_record)
|
134
|
+
end
|
135
|
+
|
136
|
+
def self.update_and_restore(record, related_data, merge)
|
137
|
+
record.update!(related_data['attributes'])
|
138
|
+
restore_associations(record, related_data['associations'], merge: merge)
|
139
|
+
end
|
140
|
+
|
141
|
+
def self.create_and_restore(associated_class, related_data, parent_record = nil, association_name = nil)
|
142
|
+
new_record = associated_class.create!(related_data['attributes'])
|
143
|
+
restore_associations(new_record, related_data['associations'], merge: false)
|
144
|
+
parent_record&.update!(association_name => new_record) if association_name
|
145
|
+
new_record
|
105
146
|
end
|
106
147
|
end
|
@@ -0,0 +1,211 @@
|
|
1
|
+
# This module provides functionality for storing and retrieving JSON-based capture data
|
2
|
+
# associated with specific records. It includes methods for saving captures, loading captures,
|
3
|
+
# and flushing (deleting) files in a specific directory.
|
4
|
+
|
5
|
+
require 'json'
|
6
|
+
require 'fileutils'
|
7
|
+
require 'pathname'
|
8
|
+
|
9
|
+
module Support
|
10
|
+
class CaptureStorage
|
11
|
+
# Constants for directory and filename constraints
|
12
|
+
CAPTURE_DIR = 'captures'.freeze
|
13
|
+
MAX_FILENAME_LENGTH = 100
|
14
|
+
VALID_FILENAME_REGEX = /\A[a-zA-Z0-9\-_\.]+\z/
|
15
|
+
|
16
|
+
class << self
|
17
|
+
# Saves capture data to a JSON file in a structured directory.
|
18
|
+
# @param record [Object] The record object (must respond to :class and :id).
|
19
|
+
# @param capture_data [Hash] The data to be saved.
|
20
|
+
# @param name [String, nil] Optional custom filename.
|
21
|
+
# @return [Hash] Result of the operation with success status and file path or error message.
|
22
|
+
def save_capture(record, capture_data, name = nil)
|
23
|
+
validate_record(record) # Ensure the record is valid
|
24
|
+
validate_capture_data(capture_data) # Ensure the capture data is valid
|
25
|
+
|
26
|
+
file_name = generate_filename(record, name) # Generate a valid filename
|
27
|
+
dir_path = create_capture_directory(record) # Create the directory for the capture
|
28
|
+
file_path = File.join(dir_path, file_name) # Full path to the capture file
|
29
|
+
|
30
|
+
write_capture_file(file_path, capture_data) # Write the capture data to the file
|
31
|
+
|
32
|
+
{ success: true, file_path: file_path }
|
33
|
+
rescue => e
|
34
|
+
{ success: false, error: e.message }
|
35
|
+
end
|
36
|
+
|
37
|
+
# Loads capture data from a JSON file.
|
38
|
+
# @param capture_file [String] Path to the capture file.
|
39
|
+
# @return [Hash] Parsed JSON data from the file.
|
40
|
+
# @raise [StorageError] If the file cannot be loaded or parsed.
|
41
|
+
def load_capture(capture_file)
|
42
|
+
validate_file_path(capture_file) # Ensure the file path is valid
|
43
|
+
|
44
|
+
file_content = read_file_safely(capture_file) # Read the file content
|
45
|
+
parsed_data = JSON.parse(file_content) # Parse the JSON content
|
46
|
+
|
47
|
+
validate_parsed_data(parsed_data) # Ensure the parsed data is valid
|
48
|
+
|
49
|
+
parsed_data
|
50
|
+
rescue => e
|
51
|
+
raise StorageError, "Failed to load capture: #{e.message}"
|
52
|
+
end
|
53
|
+
|
54
|
+
# Deletes all files in a specified directory under the capture directory.
|
55
|
+
# @param directory [String] Subdirectory name under the capture directory.
|
56
|
+
def flush(directory)
|
57
|
+
begin
|
58
|
+
dir_path = File.join(CAPTURE_DIR, directory) # Full path to the directory
|
59
|
+
|
60
|
+
unless Dir.exist?(dir_path)
|
61
|
+
raise StandardError, "Directory '#{dir_path}' does not exist."
|
62
|
+
end
|
63
|
+
|
64
|
+
files = Dir.glob("#{dir_path}/*") # Get all files in the directory
|
65
|
+
|
66
|
+
if files.empty?
|
67
|
+
puts "No files found in #{dir_path} to delete."
|
68
|
+
return
|
69
|
+
end
|
70
|
+
|
71
|
+
files.each do |file|
|
72
|
+
if File.file?(file)
|
73
|
+
begin
|
74
|
+
File.delete(file) # Delete each file
|
75
|
+
rescue Errno::EACCES
|
76
|
+
puts "Permission denied when deleting file: #{file}"
|
77
|
+
rescue StandardError => e
|
78
|
+
puts "Failed to delete file: #{file}. Error: #{e.message}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
puts "Flushed all files in #{dir_path} successfully."
|
84
|
+
|
85
|
+
rescue StandardError => e
|
86
|
+
puts "An error occurred during flush operation: #{e.message}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
# Validates that the record has the required attributes.
|
93
|
+
def validate_record(record)
|
94
|
+
unless record.respond_to?(:class) && record.respond_to?(:id)
|
95
|
+
raise ArgumentError, "Invalid record: must respond to :class and :id"
|
96
|
+
end
|
97
|
+
|
98
|
+
if record.id.nil?
|
99
|
+
raise ArgumentError, "Cannot capture unsaved record (ID is nil)"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Validates the structure of the capture data.
|
104
|
+
def validate_capture_data(data)
|
105
|
+
unless data.is_a?(Hash)
|
106
|
+
raise ArgumentError, "Capture data must be a Hash"
|
107
|
+
end
|
108
|
+
|
109
|
+
required_keys = [:model, :record_id, :attributes]
|
110
|
+
missing_keys = required_keys.reject { |k| data.key?(k) }
|
111
|
+
unless missing_keys.empty?
|
112
|
+
raise ArgumentError, "Missing required keys in capture data: #{missing_keys.join(', ')}"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Generates a valid filename for the capture file.
|
117
|
+
def generate_filename(record, custom_name)
|
118
|
+
model_name = record.class.name.downcase.gsub(/[^a-z0-9]/, '_')
|
119
|
+
record_id = record.id.to_s
|
120
|
+
timestamp = Time.now.strftime('%Y%m%d%H%M%S')
|
121
|
+
|
122
|
+
if custom_name
|
123
|
+
validate_filename(custom_name)
|
124
|
+
base_name = custom_name.gsub(/\s+/, '_')[0..MAX_FILENAME_LENGTH]
|
125
|
+
"#{base_name}.json"
|
126
|
+
else
|
127
|
+
"#{model_name}_#{record_id}_#{timestamp}.json"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Validates the custom filename.
|
132
|
+
def validate_filename(name)
|
133
|
+
if name.empty?
|
134
|
+
raise ArgumentError, "Filename cannot be empty"
|
135
|
+
end
|
136
|
+
|
137
|
+
if name.length > MAX_FILENAME_LENGTH
|
138
|
+
raise ArgumentError, "Filename too long (max #{MAX_FILENAME_LENGTH} chars)"
|
139
|
+
end
|
140
|
+
|
141
|
+
unless name.match(VALID_FILENAME_REGEX)
|
142
|
+
raise ArgumentError, "Filename contains invalid characters"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Creates the directory for storing capture files.
|
147
|
+
def create_capture_directory(record)
|
148
|
+
model_name = record.class.name.downcase.gsub(/[^a-z0-9]/, '_')
|
149
|
+
dir_path = File.join(CAPTURE_DIR, model_name)
|
150
|
+
|
151
|
+
begin
|
152
|
+
FileUtils.mkdir_p(dir_path) # Create the directory if it doesn't exist
|
153
|
+
rescue SystemCallError => e
|
154
|
+
raise StorageError, "Failed to create directory '#{dir_path}': #{e.message}"
|
155
|
+
end
|
156
|
+
|
157
|
+
dir_path
|
158
|
+
end
|
159
|
+
|
160
|
+
# Writes the capture data to a file safely.
|
161
|
+
def write_capture_file(file_path, data)
|
162
|
+
begin
|
163
|
+
temp_path = "#{file_path}.tmp"
|
164
|
+
File.write(temp_path, JSON.pretty_generate(data)) # Write to a temporary file
|
165
|
+
File.rename(temp_path, file_path) # Rename to the final file
|
166
|
+
rescue SystemCallError => e
|
167
|
+
File.delete(temp_path) if File.exist?(temp_path)
|
168
|
+
raise StorageError, "Failed to write capture file '#{file_path}': #{e.message}"
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Validates the file path for loading captures.
|
173
|
+
def validate_file_path(file_path)
|
174
|
+
unless File.exist?(file_path)
|
175
|
+
raise StorageError, "File does not exist: #{file_path}"
|
176
|
+
end
|
177
|
+
|
178
|
+
unless File.readable?(file_path)
|
179
|
+
raise StorageError, "No read permission for file: #{file_path}"
|
180
|
+
end
|
181
|
+
|
182
|
+
if File.directory?(file_path)
|
183
|
+
raise StorageError, "Path is a directory, not a file: #{file_path}"
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# Reads the file content safely.
|
188
|
+
def read_file_safely(file_path)
|
189
|
+
File.read(file_path)
|
190
|
+
rescue SystemCallError => e
|
191
|
+
raise StorageError, "Failed to read file '#{file_path}': #{e.message}"
|
192
|
+
end
|
193
|
+
|
194
|
+
# Validates the parsed JSON data structure.
|
195
|
+
def validate_parsed_data(data)
|
196
|
+
unless data.is_a?(Hash)
|
197
|
+
raise StorageError, "Invalid capture format: expected JSON object"
|
198
|
+
end
|
199
|
+
|
200
|
+
required_keys = ['model', 'record_id', 'attributes']
|
201
|
+
missing_keys = required_keys.reject { |k| data.key?(k) }
|
202
|
+
unless missing_keys.empty?
|
203
|
+
raise StorageError, "Invalid capture data: missing keys #{missing_keys.join(', ')}"
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
# Custom error class for storage-related errors.
|
209
|
+
class StorageError < StandardError; end
|
210
|
+
end
|
211
|
+
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
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'fileutils'
|
3
|
+
require_relative '../lib/active_capture'
|
4
|
+
|
5
|
+
# Test suite for the ActiveCapture.flush method
|
6
|
+
class ActiveCaptureFlushTest < Minitest::Test
|
7
|
+
# Directory where test captures will be stored
|
8
|
+
CAPTURES_DIR = 'captures'.freeze
|
9
|
+
|
10
|
+
# Setup method to prepare the test environment
|
11
|
+
def setup
|
12
|
+
# Create the captures directory
|
13
|
+
FileUtils.mkdir_p(CAPTURES_DIR)
|
14
|
+
# Create a user subdirectory
|
15
|
+
@user_dir = File.join(CAPTURES_DIR, 'user')
|
16
|
+
FileUtils.mkdir_p(@user_dir)
|
17
|
+
# Add test files to the user directory
|
18
|
+
File.write(File.join(@user_dir, 'test1.json'), '{}')
|
19
|
+
File.write(File.join(@user_dir, 'test2.json'), '{}')
|
20
|
+
end
|
21
|
+
|
22
|
+
# Teardown method to clean up after tests
|
23
|
+
def teardown
|
24
|
+
# Remove the captures directory and its contents
|
25
|
+
FileUtils.rm_rf(CAPTURES_DIR)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Test flushing a valid directory with files
|
29
|
+
def test_flush_valid_directory
|
30
|
+
# Ensure the user directory exists and contains files
|
31
|
+
assert Dir.exist?(@user_dir)
|
32
|
+
assert_equal 2, Dir.glob(File.join(@user_dir, '*')).size
|
33
|
+
|
34
|
+
# Call the flush method
|
35
|
+
ActiveCapture.flush('user')
|
36
|
+
|
37
|
+
# Verify the directory is empty after flushing
|
38
|
+
assert_empty Dir.glob(File.join(@user_dir, '*'))
|
39
|
+
puts "test_flush_valid_directory passed"
|
40
|
+
end
|
41
|
+
|
42
|
+
# Test flushing a non-existent directory
|
43
|
+
def test_flush_non_existent_directory
|
44
|
+
# Capture the output of the flush method
|
45
|
+
output = capture_io do
|
46
|
+
ActiveCapture.flush('non_existent_dir')
|
47
|
+
end
|
48
|
+
# Verify the appropriate error message is displayed
|
49
|
+
assert_match /Directory 'captures\/non_existent_dir' does not exist./, output[0]
|
50
|
+
puts "test_flush_non_existent_directory passed"
|
51
|
+
end
|
52
|
+
|
53
|
+
# Test flushing an empty directory
|
54
|
+
def test_flush_empty_directory
|
55
|
+
# Remove and recreate the user directory to ensure it's empty
|
56
|
+
FileUtils.rm_rf(@user_dir)
|
57
|
+
FileUtils.mkdir_p(@user_dir)
|
58
|
+
assert_empty Dir.glob(File.join(@user_dir, '*'))
|
59
|
+
|
60
|
+
# Capture the output of the flush method
|
61
|
+
output = capture_io do
|
62
|
+
ActiveCapture.flush('user')
|
63
|
+
end
|
64
|
+
# Verify the appropriate message is displayed for an empty directory
|
65
|
+
assert_match /No files found in captures\/user to delete./, output[0]
|
66
|
+
puts "test_flush_empty_directory passed"
|
67
|
+
end
|
68
|
+
|
69
|
+
# Test flushing a directory with a file that has restricted permissions
|
70
|
+
def test_flush_with_permission_error
|
71
|
+
# Skip this test unless run with restricted permissions
|
72
|
+
skip "Run this test with restricted permissions to verify behavior."
|
73
|
+
|
74
|
+
# Create a protected file with no permissions
|
75
|
+
protected_file = File.join(@user_dir, 'protected.json')
|
76
|
+
File.write(protected_file, '{}')
|
77
|
+
FileUtils.chmod(0o000, protected_file)
|
78
|
+
|
79
|
+
# Capture the output of the flush method
|
80
|
+
output = capture_io do
|
81
|
+
ActiveCapture.flush('user')
|
82
|
+
end
|
83
|
+
# Verify the appropriate error message is displayed for permission issues
|
84
|
+
assert_match /Permission denied when deleting file: captures\/user\/protected.json/, output[0]
|
85
|
+
|
86
|
+
# Reset permissions for cleanup
|
87
|
+
FileUtils.chmod(0o644, protected_file)
|
88
|
+
puts "test_flush_with_permission_error passed"
|
89
|
+
end
|
90
|
+
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.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tanmay Bhawsar
|
@@ -32,9 +32,11 @@ 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
|
39
|
+
- test/test_active_capture_flush.rb
|
38
40
|
- test/test_helper.rb
|
39
41
|
homepage: https://github.com/bhawsartanmay/active_capture
|
40
42
|
licenses:
|
@@ -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
|