rails_audit_log 0.7.0 → 0.8.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.
- checksums.yaml +4 -4
- data/README.md +78 -0
- data/app/concerns/rails_audit_log/auditable.rb +14 -0
- data/lib/generators/rails_audit_log/initializer/initializer_generator.rb +15 -0
- data/lib/generators/rails_audit_log/initializer/templates/rails_audit_log.rb +31 -0
- data/lib/rails_audit_log/matchers.rb +103 -0
- data/lib/rails_audit_log/minitest_assertions.rb +31 -0
- data/lib/rails_audit_log/test_helpers.rb +7 -0
- data/lib/rails_audit_log/version.rb +1 -1
- data/lib/rails_audit_log.rb +4 -0
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5cd71899d109aa978910a72ef144aeafb601dc70790e044ef723b49d1d560f65
|
|
4
|
+
data.tar.gz: 8884deec2a0abf63f14639794d6ac614689c9799e09b375ffc0cc7d788943e7d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c32ada5189eda30461ea1dbc10a67982fb0ff6c4244da4339aa3376082b38a5272bb3a1b91fe50110c08ae9ee0ae3ede52d2a43701c0f7260a4d5a6581c0f42d
|
|
7
|
+
data.tar.gz: 448becbb80428dd754bca4d1ab43625de5ec85a2c9d712e9e4a28f7c14b0c18ce6865b25dfd9ea6c46f7b1814988d0eea77ae8b6aee7a230927cc37a80f60cf3
|
data/README.md
CHANGED
|
@@ -23,6 +23,14 @@ bin/rails generate rails_audit_log:install
|
|
|
23
23
|
bin/rails db:migrate
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
Optionally scaffold a commented initializer with every configuration option:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
bin/rails generate rails_audit_log:initializer
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
This creates `config/initializers/rails_audit_log.rb` with all settings documented as commented examples inside a `RailsAuditLog.configure` block.
|
|
33
|
+
|
|
26
34
|
## Usage
|
|
27
35
|
|
|
28
36
|
### Tracking a model
|
|
@@ -384,6 +392,76 @@ To save storage at the cost of reduced reification accuracy, switch to diff-only
|
|
|
384
392
|
RailsAuditLog.store_snapshot = false
|
|
385
393
|
```
|
|
386
394
|
|
|
395
|
+
### Test helper
|
|
396
|
+
|
|
397
|
+
`without_audit_log` silences audit tracking inside the block — useful in FactoryBot factories and seed data to avoid noise in the audit trail:
|
|
398
|
+
|
|
399
|
+
```ruby
|
|
400
|
+
# spec/rails_helper.rb (or spec/support/factory_helpers.rb)
|
|
401
|
+
require "rails_audit_log/test_helpers"
|
|
402
|
+
|
|
403
|
+
RSpec.configure do |config|
|
|
404
|
+
config.include RailsAuditLog::TestHelpers
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Or include directly in FactoryBot definitions:
|
|
408
|
+
FactoryBot.define do
|
|
409
|
+
factory :post do
|
|
410
|
+
after(:create) { |p| without_audit_log { p.update!(cached_at: Time.current) } }
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
`without_audit_log` is a prefix-free wrapper around `RailsAuditLog.disable` — thread-safe and restores tracking even if the block raises.
|
|
416
|
+
|
|
417
|
+
### Minitest assertions
|
|
418
|
+
|
|
419
|
+
Add to your `test/test_helper.rb`:
|
|
420
|
+
|
|
421
|
+
```ruby
|
|
422
|
+
require "rails_audit_log/minitest_assertions"
|
|
423
|
+
|
|
424
|
+
class ActiveSupport::TestCase
|
|
425
|
+
include RailsAuditLog::MinitestAssertions
|
|
426
|
+
end
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
Then use the assertions in any test:
|
|
430
|
+
|
|
431
|
+
```ruby
|
|
432
|
+
assert_audit_log_entry post # any entry
|
|
433
|
+
assert_audit_log_entry post, event: :update # update entry
|
|
434
|
+
assert_audit_log_entry post, event: :update, touching: :title # touching title
|
|
435
|
+
refute_audit_log_entry post, event: :update # no update entry
|
|
436
|
+
assert_audit_log_entry post, event: :update, message: "custom" # custom failure message
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### RSpec matchers
|
|
440
|
+
|
|
441
|
+
Add to your `spec/rails_helper.rb` (or `spec_helper.rb`):
|
|
442
|
+
|
|
443
|
+
```ruby
|
|
444
|
+
require "rails_audit_log/matchers"
|
|
445
|
+
|
|
446
|
+
RSpec.configure do |config|
|
|
447
|
+
config.include RailsAuditLog::Matchers
|
|
448
|
+
end
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
Then use the matchers in any spec:
|
|
452
|
+
|
|
453
|
+
```ruby
|
|
454
|
+
# Assert a record has a matching audit entry
|
|
455
|
+
expect(post).to have_audit_log_entry
|
|
456
|
+
expect(post).to have_audit_log_entry(:update)
|
|
457
|
+
expect(post).to have_audit_log_entry(:update).touching(:title)
|
|
458
|
+
|
|
459
|
+
# Assert a block creates a matching audit entry
|
|
460
|
+
expect { post.update!(title: "New") }.to create_audit_log_entry
|
|
461
|
+
expect { post.update!(title: "New") }.to create_audit_log_entry(event: :update)
|
|
462
|
+
expect { post.update!(title: "New") }.to create_audit_log_entry(event: :update).touching(:title)
|
|
463
|
+
```
|
|
464
|
+
|
|
387
465
|
## Requirements
|
|
388
466
|
|
|
389
467
|
- Ruby >= 3.3
|
|
@@ -10,6 +10,8 @@ module RailsAuditLog
|
|
|
10
10
|
class_attribute :_audit_log_version_limit, default: nil
|
|
11
11
|
class_attribute :_audit_log_async, default: false
|
|
12
12
|
|
|
13
|
+
_warn_if_audit_table_missing
|
|
14
|
+
|
|
13
15
|
has_many :audit_log_entries,
|
|
14
16
|
class_name: "RailsAuditLog::AuditLogEntry",
|
|
15
17
|
as: :item,
|
|
@@ -52,6 +54,18 @@ module RailsAuditLog
|
|
|
52
54
|
end
|
|
53
55
|
|
|
54
56
|
class_methods do
|
|
57
|
+
def _warn_if_audit_table_missing
|
|
58
|
+
return if connection.table_exists?("audit_log_entries")
|
|
59
|
+
|
|
60
|
+
warn "[RailsAuditLog] WARNING: #{name} includes RailsAuditLog::Auditable but the " \
|
|
61
|
+
"'audit_log_entries' table does not exist. " \
|
|
62
|
+
"Run `bin/rails generate rails_audit_log:install && bin/rails db:migrate` to create it."
|
|
63
|
+
rescue ActiveRecord::NoDatabaseError,
|
|
64
|
+
ActiveRecord::ConnectionNotEstablished,
|
|
65
|
+
ActiveRecord::StatementInvalid
|
|
66
|
+
# DB not reachable during this phase (e.g. before db:create) — skip the check
|
|
67
|
+
end
|
|
68
|
+
|
|
55
69
|
def audit_log(only: nil, ignore: nil, meta: nil, associations: nil, version_limit: nil, async: nil)
|
|
56
70
|
self._audit_log_only = only.map(&:to_s) if only
|
|
57
71
|
self._audit_log_ignore = ignore.map(&:to_s) if ignore
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
|
|
3
|
+
module RailsAuditLog
|
|
4
|
+
module Generators
|
|
5
|
+
class InitializerGenerator < Rails::Generators::Base
|
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
|
7
|
+
|
|
8
|
+
desc "Creates a commented RailsAuditLog initializer in config/initializers."
|
|
9
|
+
|
|
10
|
+
def create_initializer_file
|
|
11
|
+
template "rails_audit_log.rb", "config/initializers/rails_audit_log.rb"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# config/initializers/rails_audit_log.rb
|
|
2
|
+
# Generated by `bin/rails generate rails_audit_log:initializer`
|
|
3
|
+
#
|
|
4
|
+
# All settings are optional. Remove the comments around any line you want to activate.
|
|
5
|
+
|
|
6
|
+
RailsAuditLog.configure do |config|
|
|
7
|
+
# Global columns excluded from all audited models. Default: ["updated_at"]
|
|
8
|
+
# config.ignored_attributes = %w[updated_at cached_at]
|
|
9
|
+
|
|
10
|
+
# Store a full attribute snapshot alongside object_changes. Default: true
|
|
11
|
+
# Disable to save storage; reify and version_at fall back to diff-only reconstruction.
|
|
12
|
+
# config.store_snapshot = false
|
|
13
|
+
|
|
14
|
+
# Capture remote_ip and user_agent into each entry's metadata column. Default: false
|
|
15
|
+
# Requires including RailsAuditLog::Controller in ApplicationController.
|
|
16
|
+
# config.capture_request_metadata = true
|
|
17
|
+
|
|
18
|
+
# Customise how the actor's display name is stored at write time. Default: actor.name
|
|
19
|
+
# config.whodunnit_display = ->(actor) { actor.email }
|
|
20
|
+
|
|
21
|
+
# Global cap on entries retained per tracked record. Default: nil (no limit)
|
|
22
|
+
# Per-model `audit_log version_limit: N` takes precedence.
|
|
23
|
+
# config.version_limit = 100
|
|
24
|
+
|
|
25
|
+
# Write all audit entries asynchronously via WriteAuditLogJob. Default: false
|
|
26
|
+
# Per-model `audit_log async: true` also works.
|
|
27
|
+
# config.async = true
|
|
28
|
+
|
|
29
|
+
# Route AuditLogEntry to a dedicated database (Rails multi-DB). Default: nil
|
|
30
|
+
# config.connects_to = { database: { writing: :audit_log, reading: :audit_log } }
|
|
31
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
module RailsAuditLog
|
|
2
|
+
module Matchers
|
|
3
|
+
def have_audit_log_entry(event = nil)
|
|
4
|
+
HaveAuditLogEntry.new(event)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def create_audit_log_entry(event: nil, touching: nil)
|
|
8
|
+
CreateAuditLogEntry.new(event: event, touching: touching)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class HaveAuditLogEntry
|
|
12
|
+
def initialize(event)
|
|
13
|
+
@event = event
|
|
14
|
+
@touching = nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def touching(attribute)
|
|
18
|
+
@touching = attribute
|
|
19
|
+
self
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def matches?(record)
|
|
23
|
+
@record = record
|
|
24
|
+
scope = record.audit_log_entries
|
|
25
|
+
scope = scope.where(event: @event.to_s) if @event
|
|
26
|
+
scope = scope.touching(@touching) if @touching
|
|
27
|
+
scope.exists?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def failure_message
|
|
31
|
+
"expected #{@record.class}##{@record.id} to have an audit log entry#{qualifier}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def failure_message_when_negated
|
|
35
|
+
"expected #{@record.class}##{@record.id} not to have an audit log entry#{qualifier}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def description
|
|
39
|
+
"have an audit log entry#{qualifier}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def qualifier
|
|
45
|
+
parts = []
|
|
46
|
+
parts << " with event '#{@event}'" if @event
|
|
47
|
+
parts << " touching '#{@touching}'" if @touching
|
|
48
|
+
parts.join
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class CreateAuditLogEntry
|
|
53
|
+
def initialize(event:, touching:)
|
|
54
|
+
@event = event
|
|
55
|
+
@touching = touching
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def touching(attribute)
|
|
59
|
+
@touching = attribute
|
|
60
|
+
self
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def supports_block_expectations?
|
|
64
|
+
true
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def matches?(block)
|
|
68
|
+
@before = matching_scope.count
|
|
69
|
+
block.call
|
|
70
|
+
@after = matching_scope.count
|
|
71
|
+
@after > @before
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def failure_message
|
|
75
|
+
"expected block to create an audit log entry#{qualifier}, but none was created"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def failure_message_when_negated
|
|
79
|
+
"expected block not to create an audit log entry#{qualifier}, but one was created"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def description
|
|
83
|
+
"create an audit log entry#{qualifier}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def matching_scope
|
|
89
|
+
scope = RailsAuditLog::AuditLogEntry.all
|
|
90
|
+
scope = scope.where(event: @event.to_s) if @event
|
|
91
|
+
scope = scope.touching(@touching) if @touching
|
|
92
|
+
scope
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def qualifier
|
|
96
|
+
parts = []
|
|
97
|
+
parts << " with event '#{@event}'" if @event
|
|
98
|
+
parts << " touching '#{@touching}'" if @touching
|
|
99
|
+
parts.join
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module RailsAuditLog
|
|
2
|
+
module MinitestAssertions
|
|
3
|
+
def assert_audit_log_entry(record, event: nil, touching: nil, message: nil)
|
|
4
|
+
scope = build_scope(record, event, touching)
|
|
5
|
+
msg = message || default_message("to have", record, event, touching)
|
|
6
|
+
assert scope.exists?, msg
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def refute_audit_log_entry(record, event: nil, touching: nil, message: nil)
|
|
10
|
+
scope = build_scope(record, event, touching)
|
|
11
|
+
msg = message || default_message("not to have", record, event, touching)
|
|
12
|
+
refute scope.exists?, msg
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def build_scope(record, event, touching)
|
|
18
|
+
scope = record.audit_log_entries
|
|
19
|
+
scope = scope.where(event: event.to_s) if event
|
|
20
|
+
scope = scope.touching(touching) if touching
|
|
21
|
+
scope
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def default_message(polarity, record, event, touching)
|
|
25
|
+
parts = []
|
|
26
|
+
parts << " with event '#{event}'" if event
|
|
27
|
+
parts << " touching '#{touching}'" if touching
|
|
28
|
+
"Expected #{record.class}##{record.id} #{polarity} an audit log entry#{parts.join}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/rails_audit_log.rb
CHANGED
|
@@ -5,6 +5,10 @@ module RailsAuditLog
|
|
|
5
5
|
# Global default columns to ignore across all audited models.
|
|
6
6
|
# Override in an initializer: RailsAuditLog.ignored_attributes = %w[updated_at cached_at]
|
|
7
7
|
mattr_accessor :ignored_attributes, default: %w[updated_at]
|
|
8
|
+
|
|
9
|
+
def self.configure
|
|
10
|
+
yield self
|
|
11
|
+
end
|
|
8
12
|
mattr_accessor :store_snapshot, default: true
|
|
9
13
|
mattr_accessor :capture_request_metadata, default: false
|
|
10
14
|
mattr_accessor :version_limit, default: nil
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_audit_log
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chuck Smith
|
|
@@ -46,10 +46,15 @@ files:
|
|
|
46
46
|
- app/models/rails_audit_log/audit_log_entry.rb
|
|
47
47
|
- app/views/layouts/rails_audit_log/application.html.erb
|
|
48
48
|
- config/routes.rb
|
|
49
|
+
- lib/generators/rails_audit_log/initializer/initializer_generator.rb
|
|
50
|
+
- lib/generators/rails_audit_log/initializer/templates/rails_audit_log.rb
|
|
49
51
|
- lib/generators/rails_audit_log/install/install_generator.rb
|
|
50
52
|
- lib/generators/rails_audit_log/install/templates/create_audit_log_entries.rb
|
|
51
53
|
- lib/rails_audit_log.rb
|
|
52
54
|
- lib/rails_audit_log/engine.rb
|
|
55
|
+
- lib/rails_audit_log/matchers.rb
|
|
56
|
+
- lib/rails_audit_log/minitest_assertions.rb
|
|
57
|
+
- lib/rails_audit_log/test_helpers.rb
|
|
53
58
|
- lib/rails_audit_log/version.rb
|
|
54
59
|
- lib/tasks/rails_audit_log_tasks.rake
|
|
55
60
|
homepage: https://github.com/eclectic-coding/rails_audit_log
|