rails_audit_log 0.9.0 → 1.0.0

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.
@@ -1,4 +1,23 @@
1
1
  module RailsAuditLog
2
+ # Represents a single audited event (+create+, +update+, or +destroy+) for
3
+ # one ActiveRecord record.
4
+ #
5
+ # == Columns
6
+ #
7
+ # [event] One of <tt>"create"</tt>, <tt>"update"</tt>, <tt>"destroy"</tt>.
8
+ # [item_type] Class name of the audited record (e.g. <tt>"Article"</tt>).
9
+ # [item_id] Primary key of the audited record.
10
+ # [object_changes] JSON hash of attribute changes in <tt>[from, to]</tt> form.
11
+ # All three event types use the same format:
12
+ # +create+ stores <tt>[nil, new]</tt> for every column,
13
+ # +update+ stores <tt>[old, new]</tt> for changed columns only,
14
+ # +destroy+ stores <tt>[final, nil]</tt> for every column.
15
+ # [object] JSON snapshot of the record's full attributes before the
16
+ # change (stored when {RailsAuditLog.store_snapshot} is +true+).
17
+ # [whodunnit_snapshot] Display name of the actor at the time of the change.
18
+ # [actor_type / actor_id] Polymorphic reference to the actor record.
19
+ # [reason] Optional free-text reason string.
20
+ # [metadata] Arbitrary JSON hash (request IP, custom lambdas, etc.).
2
21
  class AuditLogEntry < ApplicationRecord
3
22
  self.table_name = "audit_log_entries"
4
23
 
@@ -6,33 +25,63 @@ module RailsAuditLog
6
25
  BLOB_COLUMNS = %w[object_changes object metadata].freeze
7
26
  PERIODS = { "1h" => 1.hour, "24h" => 24.hours, "7d" => 7.days }.freeze
8
27
 
28
+ # @api private
9
29
  def self.configure_connection!
10
30
  return unless (opts = RailsAuditLog.connects_to)
11
31
 
12
32
  connects_to(**opts)
13
33
  end
14
34
 
15
- belongs_to :item, polymorphic: true, optional: true
35
+ belongs_to :item, polymorphic: true, optional: true
16
36
  belongs_to :actor, polymorphic: true, optional: true
17
37
 
18
- validates :event, presence: true, inclusion: { in: EVENTS }
38
+ validates :event, presence: true, inclusion: { in: EVENTS }
19
39
  validates :item_type, presence: true
20
- validates :item_id, presence: true
21
- validate :metadata_must_be_a_hash
40
+ validates :item_id, presence: true
41
+ validate :metadata_must_be_a_hash
22
42
 
23
- # Event scopes
43
+ # @!group Event scopes
44
+
45
+ # Entries for +create+ events.
46
+ # @return [ActiveRecord::Relation]
24
47
  scope :created_events, -> { where(event: "create") }
48
+
49
+ # Entries for +update+ events.
50
+ # @return [ActiveRecord::Relation]
25
51
  scope :updated_events, -> { where(event: "update") }
26
- scope :destroyed_events, -> { where(event: "destroy") }
27
52
 
28
- # Deprecated short aliases kept for backwards compatibility
53
+ # Entries for +destroy+ events.
54
+ # @return [ActiveRecord::Relation]
55
+ scope :destroyed_events, -> { where(event: "destroy") }
29
56
 
57
+ # @deprecated Use {.created_events} instead.
30
58
  scope :creates, -> { created_events }
59
+ # @deprecated Use {.updated_events} instead.
31
60
  scope :updates, -> { updated_events }
61
+ # @deprecated Use {.destroyed_events} instead.
32
62
  scope :destroys, -> { destroyed_events }
33
63
 
34
- # Actor / resource scopes
64
+ # @!endgroup
65
+
66
+ # @!group Actor / resource scopes
67
+
68
+ # Entries written by a specific actor.
69
+ #
70
+ # @param actor [ActiveRecord::Base] the actor record to filter by
71
+ # @return [ActiveRecord::Relation]
72
+ # @example
73
+ # AuditLogEntry.by_actor(current_user)
35
74
  scope :by_actor, ->(actor) { where(actor_type: actor.class.name, actor_id: actor.id) }
75
+
76
+ # Entries for a specific resource class or instance.
77
+ # Pass a class to get all entries for that type; pass an instance for one record.
78
+ #
79
+ # @param resource [Class, ActiveRecord::Base]
80
+ # @return [ActiveRecord::Relation]
81
+ # @example All entries for Article
82
+ # AuditLogEntry.for_resource(Article)
83
+ # @example All entries for one article
84
+ # AuditLogEntry.for_resource(article)
36
85
  scope :for_resource, lambda { |resource|
37
86
  if resource.is_a?(Class)
38
87
  where(item_type: resource.name)
@@ -41,16 +90,61 @@ module RailsAuditLog
41
90
  end
42
91
  }
43
92
 
44
- # Time scopes
45
- scope :since, ->(time) { where(created_at: time..) }
46
- scope :until, ->(time) { where(created_at: ..time) }
93
+ # @!endgroup
94
+
95
+ # @!group Time scopes
96
+
97
+ # Entries created at or after +time+.
98
+ #
99
+ # @param time [Time]
100
+ # @return [ActiveRecord::Relation]
101
+ scope :since, ->(time) { where(created_at: time..) }
102
+
103
+ # Entries created at or before +time+.
104
+ #
105
+ # @param time [Time]
106
+ # @return [ActiveRecord::Relation]
107
+ scope :until, ->(time) { where(created_at: ..time) }
108
+
109
+ # Entries within a named period. Valid keys: <tt>"1h"</tt>, <tt>"24h"</tt>, <tt>"7d"</tt>.
110
+ #
111
+ # @param period [String] one of +PERIODS.keys+
112
+ # @return [ActiveRecord::Relation]
47
113
  scope :for_period, ->(period) { where(created_at: PERIODS[period].ago..) }
48
114
 
49
- # Projection scope — omits JSON blob columns for index/listing queries
115
+ # @!endgroup
116
+
117
+ # Omits the three JSON blob columns (+object_changes+, +object+, +metadata+)
118
+ # from the +SELECT+. Use on index/listing queries where blobs are not
119
+ # displayed to reduce I/O and avoid deserializing large payloads.
120
+ #
121
+ # @return [ActiveRecord::Relation]
50
122
  scope :slim, -> { select(column_names - BLOB_COLUMNS) }
51
123
 
52
- # Instance methods
124
+ # Entries where +object_changes+ contains a key matching +attribute+.
125
+ # Uses <tt>json_extract</tt> on SQLite/MySQL and <tt>->></tt> on PostgreSQL.
126
+ #
127
+ # @param attribute [Symbol, String] the attribute name to filter on
128
+ # @return [ActiveRecord::Relation]
129
+ # @example
130
+ # AuditLogEntry.touching(:title)
131
+ # post.audit_log_entries.updated_events.touching(:published_at)
132
+ scope :touching, ->(attribute) {
133
+ if connection.adapter_name =~ /PostgreSQL/i
134
+ # :nocov:
135
+ where("object_changes->>? IS NOT NULL", attribute.to_s)
136
+ # :nocov:
137
+ else
138
+ where("json_extract(object_changes, ?) IS NOT NULL", "$.#{attribute}")
139
+ end
140
+ }
53
141
 
142
+ # Reconstructs and returns the record's state *before* this entry's change.
143
+ # Uses the +object+ snapshot when available; falls back to deriving prior
144
+ # state from +object_changes+ (or the live record for +update+ entries).
145
+ #
146
+ # @return [ActiveRecord::Base, nil] an unpersisted instance; +nil+ for
147
+ # +create+ entries (there is no prior state)
54
148
  def reify
55
149
  return nil if event == "create"
56
150
 
@@ -82,18 +176,37 @@ module RailsAuditLog
82
176
  instance
83
177
  end
84
178
 
179
+ # Returns the entry immediately before this one in the version chain for
180
+ # the same record (lower +id+), or +nil+ if this is the first entry.
181
+ #
182
+ # @return [AuditLogEntry, nil]
85
183
  def previous
86
184
  self.class.where(item_type: item_type, item_id: item_id).where("id < ?", id).order(id: :desc).first
87
185
  end
88
186
 
187
+ # Returns the entry immediately after this one in the version chain for
188
+ # the same record (higher +id+), or +nil+ if this is the last entry.
189
+ #
190
+ # @return [AuditLogEntry, nil]
89
191
  def next
90
192
  self.class.where(item_type: item_type, item_id: item_id).where("id > ?", id).order(id: :asc).first
91
193
  end
92
194
 
195
+ # Returns the list of attribute (and association) names that changed in
196
+ # this entry, derived from the keys of +object_changes+.
197
+ #
198
+ # @return [Array<String>]
93
199
  def changed_attributes
94
200
  object_changes&.keys || []
95
201
  end
96
202
 
203
+ # Returns +object_changes+ in a named-key format convenient for display.
204
+ #
205
+ # @return [Hash{String => Hash}] keys are attribute names; values are
206
+ # hashes with +:from+ and +:to+ keys
207
+ # @example
208
+ # entry.diff
209
+ # # => { "title" => { from: "Old", to: "New" }, ... }
97
210
  def diff
98
211
  return {} unless object_changes
99
212
 
@@ -105,18 +218,5 @@ module RailsAuditLog
105
218
  def metadata_must_be_a_hash
106
219
  errors.add(:metadata, "must be a Hash") if metadata.present? && !metadata.is_a?(Hash)
107
220
  end
108
-
109
- public
110
-
111
- # Attribute scope — uses json_extract (SQLite/MySQL) or ->> (PostgreSQL)
112
- scope :touching, ->(attribute) {
113
- if connection.adapter_name =~ /PostgreSQL/i
114
- # :nocov:
115
- where("object_changes->>? IS NOT NULL", attribute.to_s)
116
- # :nocov:
117
- else
118
- where("json_extract(object_changes, ?) IS NOT NULL", "$.#{attribute}")
119
- end
120
- }
121
221
  end
122
222
  end
@@ -0,0 +1,21 @@
1
+ require "rails/generators"
2
+ require "rails/generators/active_record"
3
+
4
+ module RailsAuditLog
5
+ module Generators
6
+ class MigrateFromPaperTrailGenerator < Rails::Generators::Base
7
+ include ActiveRecord::Generators::Migration
8
+
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ desc "Creates a data migration that copies PaperTrail versions to audit_log_entries."
12
+
13
+ def create_migration_file
14
+ migration_template(
15
+ "migrate_from_paper_trail.rb",
16
+ "db/migrate/migrate_from_paper_trail.rb"
17
+ )
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,99 @@
1
+ require "yaml"
2
+ require "json"
3
+
4
+ # Data migration: copies PaperTrail `versions` rows to `audit_log_entries`.
5
+ #
6
+ # Column mapping:
7
+ # versions.item_type → audit_log_entries.item_type
8
+ # versions.item_id → audit_log_entries.item_id
9
+ # versions.event → audit_log_entries.event (create/update/destroy only)
10
+ # versions.object_changes → audit_log_entries.object_changes (YAML or JSON → JSON)
11
+ # versions.object → audit_log_entries.object (YAML or JSON → JSON)
12
+ # versions.whodunnit → audit_log_entries.whodunnit_snapshot
13
+ # versions.created_at → audit_log_entries.created_at
14
+ #
15
+ # actor_type / actor_id are not populated — PaperTrail stores the actor
16
+ # only as a string (whodunnit), so polymorphic references cannot be inferred.
17
+ #
18
+ # This migration is irreversible.
19
+ class MigrateFromPaperTrail < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
20
+ BATCH_SIZE = 1_000
21
+ VALID_EVENTS = %w[create update destroy].freeze
22
+
23
+ # Isolated AR classes to avoid coupling to the host app's models.
24
+ class Version < ActiveRecord::Base # @api private
25
+ self.table_name = "versions"
26
+ end
27
+
28
+ class AuditEntry < ActiveRecord::Base # @api private
29
+ self.table_name = "audit_log_entries"
30
+ end
31
+
32
+ def up
33
+ unless table_exists?(:versions)
34
+ say " versions table not found — nothing to migrate."
35
+ return
36
+ end
37
+
38
+ total = Version.count
39
+ migrated = 0
40
+ skipped = 0
41
+
42
+ say_with_time "Migrating #{total} PaperTrail versions → audit_log_entries" do
43
+ Version.in_batches(of: BATCH_SIZE) do |batch|
44
+ rows = batch.filter_map do |v|
45
+ next unless VALID_EVENTS.include?(v.event)
46
+
47
+ {
48
+ event: v.event,
49
+ item_type: v.item_type,
50
+ item_id: v.item_id,
51
+ object_changes: parse_serialized(v.read_attribute_before_type_cast(:object_changes)),
52
+ object: parse_serialized(v.read_attribute_before_type_cast(:object)),
53
+ whodunnit_snapshot: v.whodunnit,
54
+ created_at: v.created_at,
55
+ updated_at: v.created_at
56
+ }
57
+ end
58
+
59
+ skipped += batch.size - rows.size
60
+ migrated += rows.size
61
+ AuditEntry.insert_all(rows) if rows.any?
62
+ end
63
+ end
64
+
65
+ say " Migrated: #{migrated} Skipped (unsupported event): #{skipped}"
66
+ end
67
+
68
+ def down
69
+ raise ActiveRecord::IrreversibleMigration,
70
+ "Cannot reverse PaperTrail migration — rows already written to audit_log_entries " \
71
+ "cannot be reliably distinguished from native entries."
72
+ end
73
+
74
+ private
75
+
76
+ # Parses a PaperTrail serialized column (YAML or JSON) and returns a Hash,
77
+ # or nil if the value is blank or unparseable.
78
+ def parse_serialized(value)
79
+ return nil if value.nil? || value.to_s.strip.empty?
80
+
81
+ begin
82
+ parsed = JSON.parse(value)
83
+ return parsed.is_a?(Hash) ? parsed : nil
84
+ rescue JSON::ParserError
85
+ # fall through to YAML
86
+ end
87
+
88
+ begin
89
+ parsed = YAML.safe_load(
90
+ value,
91
+ permitted_classes: [Symbol, Date, Time, DateTime, BigDecimal,
92
+ ActiveSupport::TimeWithZone, ActiveSupport::Duration]
93
+ )
94
+ parsed.is_a?(Hash) ? parsed : nil
95
+ rescue StandardError
96
+ nil
97
+ end
98
+ end
99
+ end
@@ -4,6 +4,7 @@ require "pagy"
4
4
  require "pagy/toolbox/paginators/method"
5
5
 
6
6
  module RailsAuditLog
7
+ # @api private
7
8
  class Engine < ::Rails::Engine
8
9
  isolate_namespace RailsAuditLog
9
10
 
@@ -1,24 +1,67 @@
1
1
  module RailsAuditLog
2
+ # RSpec matchers for asserting audit log behaviour.
3
+ #
4
+ # == Setup
5
+ #
6
+ # # spec/rails_helper.rb
7
+ # require "rails_audit_log/matchers"
8
+ #
9
+ # RSpec.configure do |config|
10
+ # config.include RailsAuditLog::Matchers
11
+ # end
12
+ #
13
+ # == Usage
14
+ #
15
+ # # Assert a record already has an entry
16
+ # expect(post).to have_audit_log_entry(:update).touching(:title)
17
+ #
18
+ # # Assert a block creates a new entry
19
+ # expect { post.update!(title: "New") }.to create_audit_log_entry(event: :update, touching: :title)
2
20
  module Matchers
21
+ # Returns a matcher that asserts the record has at least one matching
22
+ # {AuditLogEntry}.
23
+ #
24
+ # @param event [Symbol, String, nil] optional event filter
25
+ # (<tt>:create</tt>, <tt>:update</tt>, or <tt>:destroy</tt>)
26
+ # @return [HaveAuditLogEntry]
27
+ # @example
28
+ # expect(post).to have_audit_log_entry
29
+ # expect(post).to have_audit_log_entry(:update)
30
+ # expect(post).to have_audit_log_entry(:update).touching(:title)
3
31
  def have_audit_log_entry(event = nil)
4
32
  HaveAuditLogEntry.new(event)
5
33
  end
6
34
 
35
+ # Returns a matcher that asserts the block creates at least one new
36
+ # {AuditLogEntry} matching the given filters.
37
+ #
38
+ # @param event [Symbol, String, nil] optional event filter
39
+ # @param touching [Symbol, String, nil] optional attribute filter
40
+ # @return [CreateAuditLogEntry]
41
+ # @example
42
+ # expect { post.update!(title: "x") }.to create_audit_log_entry(event: :update)
43
+ # expect { post.update!(title: "x") }.to create_audit_log_entry(touching: :title)
7
44
  def create_audit_log_entry(event: nil, touching: nil)
8
45
  CreateAuditLogEntry.new(event: event, touching: touching)
9
46
  end
10
47
 
48
+ # RSpec matcher — asserts that a record already has a matching entry.
11
49
  class HaveAuditLogEntry
12
50
  def initialize(event)
13
51
  @event = event
14
52
  @touching = nil
15
53
  end
16
54
 
55
+ # Chains an attribute filter onto the matcher.
56
+ #
57
+ # @param attribute [Symbol, String]
58
+ # @return [self]
17
59
  def touching(attribute)
18
60
  @touching = attribute
19
61
  self
20
62
  end
21
63
 
64
+ # @api private
22
65
  def matches?(record)
23
66
  @record = record
24
67
  scope = record.audit_log_entries
@@ -27,14 +70,17 @@ module RailsAuditLog
27
70
  scope.exists?
28
71
  end
29
72
 
73
+ # @api private
30
74
  def failure_message
31
75
  "expected #{@record.class}##{@record.id} to have an audit log entry#{qualifier}"
32
76
  end
33
77
 
78
+ # @api private
34
79
  def failure_message_when_negated
35
80
  "expected #{@record.class}##{@record.id} not to have an audit log entry#{qualifier}"
36
81
  end
37
82
 
83
+ # @api private
38
84
  def description
39
85
  "have an audit log entry#{qualifier}"
40
86
  end
@@ -49,21 +95,28 @@ module RailsAuditLog
49
95
  end
50
96
  end
51
97
 
98
+ # RSpec matcher — asserts that a block creates a new matching entry.
52
99
  class CreateAuditLogEntry
53
100
  def initialize(event:, touching:)
54
101
  @event = event
55
102
  @touching = touching
56
103
  end
57
104
 
105
+ # Chains an attribute filter onto the matcher.
106
+ #
107
+ # @param attribute [Symbol, String]
108
+ # @return [self]
58
109
  def touching(attribute)
59
110
  @touching = attribute
60
111
  self
61
112
  end
62
113
 
114
+ # @api private
63
115
  def supports_block_expectations?
64
116
  true
65
117
  end
66
118
 
119
+ # @api private
67
120
  def matches?(block)
68
121
  @before = matching_scope.count
69
122
  block.call
@@ -71,14 +124,17 @@ module RailsAuditLog
71
124
  @after > @before
72
125
  end
73
126
 
127
+ # @api private
74
128
  def failure_message
75
129
  "expected block to create an audit log entry#{qualifier}, but none was created"
76
130
  end
77
131
 
132
+ # @api private
78
133
  def failure_message_when_negated
79
134
  "expected block not to create an audit log entry#{qualifier}, but one was created"
80
135
  end
81
136
 
137
+ # @api private
82
138
  def description
83
139
  "create an audit log entry#{qualifier}"
84
140
  end
@@ -1,11 +1,47 @@
1
1
  module RailsAuditLog
2
+ # Opt-in Minitest assertions for audit log expectations.
3
+ #
4
+ # == Setup
5
+ #
6
+ # # test/test_helper.rb
7
+ # require "rails_audit_log/minitest_assertions"
8
+ #
9
+ # class ActiveSupport::TestCase
10
+ # include RailsAuditLog::MinitestAssertions
11
+ # end
12
+ #
13
+ # == Usage
14
+ #
15
+ # assert_audit_log_entry(post, event: :update, touching: :title)
16
+ # refute_audit_log_entry(post, event: :create)
2
17
  module MinitestAssertions
18
+ # Asserts that +record+ has at least one {AuditLogEntry} matching the
19
+ # given filters. Fails with a descriptive message when no entry is found.
20
+ #
21
+ # @param record [ActiveRecord::Base] the audited record to check
22
+ # @param event [Symbol, String, nil] optional event filter
23
+ # (<tt>:create</tt>, <tt>:update</tt>, or <tt>:destroy</tt>)
24
+ # @param touching [Symbol, String, nil] optional attribute name filter
25
+ # @param message [String, nil] custom failure message; auto-generated when nil
26
+ # @return [void]
27
+ # @example
28
+ # assert_audit_log_entry(post, event: :update, touching: :title)
3
29
  def assert_audit_log_entry(record, event: nil, touching: nil, message: nil)
4
30
  scope = build_scope(record, event, touching)
5
31
  msg = message || default_message("to have", record, event, touching)
6
32
  assert scope.exists?, msg
7
33
  end
8
34
 
35
+ # Asserts that +record+ has *no* {AuditLogEntry} matching the given filters.
36
+ # Fails with a descriptive message when a matching entry is found.
37
+ #
38
+ # @param record [ActiveRecord::Base] the audited record to check
39
+ # @param event [Symbol, String, nil] optional event filter
40
+ # @param touching [Symbol, String, nil] optional attribute name filter
41
+ # @param message [String, nil] custom failure message; auto-generated when nil
42
+ # @return [void]
43
+ # @example
44
+ # refute_audit_log_entry(post, event: :destroy)
9
45
  def refute_audit_log_entry(record, event: nil, touching: nil, message: nil)
10
46
  scope = build_scope(record, event, touching)
11
47
  msg = message || default_message("not to have", record, event, touching)
@@ -0,0 +1,76 @@
1
+ module RailsAuditLog
2
+ # Opt-in compatibility shim for gradual migration from PaperTrail.
3
+ # Include alongside RailsAuditLog::Auditable to keep PaperTrail's
4
+ # familiar API while your codebase migrates.
5
+ #
6
+ # @example
7
+ # class Article < ApplicationRecord
8
+ # include RailsAuditLog::Auditable
9
+ # include RailsAuditLog::PaperTrailCompat
10
+ # end
11
+ #
12
+ # article.versions # audit_log_entries, oldest-first
13
+ # article.paper_trail.version # most recent AuditLogEntry
14
+ # article.paper_trail.previous_version # reconstructed previous state
15
+ # article.paper_trail.originator # whodunnit_snapshot string
16
+ # article.paper_trail.version_at(1.week.ago) # time-travel reconstruction
17
+ module PaperTrailCompat
18
+ extend ActiveSupport::Concern
19
+
20
+ included do
21
+ has_many :versions,
22
+ -> { order(created_at: :asc, id: :asc) },
23
+ class_name: "RailsAuditLog::AuditLogEntry",
24
+ as: :item,
25
+ dependent: :destroy
26
+ end
27
+
28
+ # Returns a proxy object exposing PaperTrail's instance-level API.
29
+ #
30
+ # @return [Proxy]
31
+ def paper_trail
32
+ @paper_trail_proxy ||= Proxy.new(self)
33
+ end
34
+
35
+ # Proxy providing the PaperTrail instance API surface backed by
36
+ # {AuditLogEntry} records.
37
+ class Proxy
38
+ # @param record [ActiveRecord::Base] the record this proxy wraps
39
+ def initialize(record)
40
+ @record = record
41
+ end
42
+
43
+ # The most recent {AuditLogEntry} for the record.
44
+ #
45
+ # @return [AuditLogEntry, nil]
46
+ def version
47
+ @record.audit_log_entries.order(id: :desc).first
48
+ end
49
+
50
+ # Reconstructs the record's state before the most recent change.
51
+ # Returns +nil+ for newly created records (no prior state exists).
52
+ #
53
+ # @return [ActiveRecord::Base, nil] an unpersisted instance; or +nil+
54
+ def previous_version
55
+ version&.reify
56
+ end
57
+
58
+ # Display name of the actor who made the most recent change, as stored
59
+ # in +whodunnit_snapshot+.
60
+ #
61
+ # @return [String, nil]
62
+ def originator
63
+ version&.whodunnit_snapshot
64
+ end
65
+
66
+ # Reconstructs the record's state as it was at +timestamp+.
67
+ # Delegates to {RailsAuditLog.version_at}.
68
+ #
69
+ # @param timestamp [Time]
70
+ # @return [ActiveRecord::Base, nil] an unpersisted instance; or +nil+
71
+ def version_at(timestamp)
72
+ RailsAuditLog.version_at(@record, timestamp)
73
+ end
74
+ end
75
+ end
76
+ end
@@ -1,5 +1,25 @@
1
1
  module RailsAuditLog
2
+ # Opt-in test helper for suppressing audit writes in test setup code.
3
+ #
4
+ # == Setup
5
+ #
6
+ # # spec/rails_helper.rb
7
+ # require "rails_audit_log/test_helpers"
8
+ #
9
+ # RSpec.configure do |config|
10
+ # config.include RailsAuditLog::TestHelpers
11
+ # end
12
+ #
13
+ # == Usage
14
+ #
15
+ # let(:post) { without_audit_log { Post.create!(title: "fixture") } }
2
16
  module TestHelpers
17
+ # Executes the block with audit logging disabled. A prefix-free wrapper
18
+ # around {RailsAuditLog.disable} intended for use in FactoryBot factories,
19
+ # +let+ blocks, and other test setup where audit noise is unwanted.
20
+ #
21
+ # @yield executes the block without recording any audit entries
22
+ # @return [Object] the return value of the block
3
23
  def without_audit_log(&block)
4
24
  RailsAuditLog.disable(&block)
5
25
  end
@@ -1,3 +1,3 @@
1
1
  module RailsAuditLog
2
- VERSION = "0.9.0"
2
+ VERSION = "1.0.0"
3
3
  end