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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '03486bb96c7c947866d02dbd2a0d3427c81b8ccaea70440ce5ff4463a1905c43'
4
- data.tar.gz: ba44f0ab8b7d31f34a9c2470031569514bc5346ad1acbd328a74d2f06ddb7b27
3
+ metadata.gz: 93b2a116c7a013ba34b42a5211179f14185d2e60d324ff74da5f4f23a8f30a7d
4
+ data.tar.gz: dd339d97072f5440c0ef2551c73aa6708bc9e22f894fda9e5641eb463d013399
5
5
  SHA512:
6
- metadata.gz: e116aa0888362fd7f400bff904af9051a9aaaf60c941bd609008db77940c1007e5f20369ac5490cf918355cb3cfe693677074be0b98500e7346fd60f54fcccee
7
- data.tar.gz: d3857c7bea275437fe8af8df3337c33da86bd2dfcc8738faea767c4081934381bfc14d3911c34a500f02d5bee0f219858f493af79b259e27fa2030e190578e6d
6
+ metadata.gz: 17dde4d63324dc3f3e8934e8e5310b3484be942e653068ff9e6d97474d52641571749a556f40985164b9067f3669ed1979f2ccf692479dd998e59c380c549ec9
7
+ data.tar.gz: 2f0111ff053df52edec0f1632716e45224f1447a811253543212766d685a9d881e7d2b0ec6841cee696bf99ff05c570beff835bfb4daf4674f1a5db3cd493fb5
@@ -1,106 +1,147 @@
1
- require 'active_capture/version'
2
- require_relative 'active_capture/capture_storage'
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
- class Capture
8
- def self.take(record, associations: [], name: nil)
9
- raise ArgumentError, 'Record must be an ActiveRecord::Base instance' unless record.is_a?(ActiveRecord::Base)
10
-
11
- capture_data = {
12
- model: record.class.name,
13
- record_id: record.id,
14
- attributes: record.attributes,
15
- associations: capture_associations(record, associations)
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
- def self.restore(record, capture_file, merge: false)
22
- captured_records = CaptureStorage.load_capture(capture_file)
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
- ActiveRecord::Base.transaction do
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
- private
38
+ def self.flush(directory)
39
+ Support::CaptureStorage.flush(directory)
40
+ end
33
41
 
34
- def self.capture_associations(record, associations)
35
- association_data = {}
42
+ private
36
43
 
37
- associations.each do |association|
38
- nested_associations = []
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
- if association.is_a?(Hash)
41
- association_name = association.keys.first
42
- nested_associations = association.values.flatten
43
- else
44
- association_name = association
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
- if record.respond_to?(association_name)
48
- related_records = record.send(association_name)
49
-
50
- if related_records.is_a?(ActiveRecord::Base)
51
- association_data[association_name] = {
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
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
- association_data
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
- def self.restore_associations(record, associations_data, merge: false)
70
- associations_data.each do |association_name, related_data|
71
- if related_data.is_a?(Array)
72
- associated_class = record.class.reflect_on_association(association_name).klass
73
- existing_records = record.send(association_name)
74
-
75
- related_data.each do |related_record_data|
76
- if related_record_data['attributes']['id'] && existing_record = existing_records.find_by(id: related_record_data['attributes']['id'])
77
- existing_record.update!(related_record_data['attributes'])
78
- restore_associations(existing_record, related_record_data['associations'], merge: merge)
79
- else
80
- new_record = associated_class.create!(related_record_data['attributes'])
81
- restore_associations(new_record, related_record_data['associations'], merge: merge)
82
- end
83
- end
84
- else
85
- associated_class = record.class.reflect_on_association(association_name).klass
86
- if merge && related_data['attributes']['id']
87
- existing_record = record.send(association_name)
88
-
89
- if existing_record && existing_record.id == related_data['attributes']['id']
90
- existing_record.update!(related_data['attributes'])
91
- restore_associations(existing_record, related_data['associations'], merge: merge)
92
- else
93
- new_record = associated_class.create!(related_data['attributes'])
94
- record.update!(association_name => new_record)
95
- restore_associations(new_record, related_data['associations'], merge: merge)
96
- end
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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Support
4
+ VERSION = "1.1.2"
5
+ 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
@@ -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
- ActiveCapture::Capture.take(@user, associations: [:posts, { posts: :comments }])
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
- ActiveCapture::Capture.take(@user, associations: [:posts, { posts: :comments }])
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.0
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/active_capture/capture_storage.rb
36
- - lib/active_capture/version.rb
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
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActiveCapture
4
- VERSION = "1.1.0"
5
- end