audit 0.1.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.
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ source :gemcutter
2
+
3
+ gem "cassandra", "~> 0.8.2"
4
+ gem "activemodel", "~> 3.0.0"
5
+ gem "yajl-ruby", "~> 0.7.7"
6
+
7
+ group :development do
8
+ gem "activerecord", "~> 3.0.0"
9
+ gem "sqlite3-ruby"
10
+ end
11
+
12
+ group :test do
13
+ gem "shoulda", "~> 2.11.3"
14
+ gem "nokogiri", "~> 1.4.3.1" # Cassandra::Mock needs this
15
+ gem "flexmock", "~> 0.8.7"
16
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,47 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ activemodel (3.0.0)
5
+ activesupport (= 3.0.0)
6
+ builder (~> 2.1.2)
7
+ i18n (~> 0.4.1)
8
+ activerecord (3.0.0)
9
+ activemodel (= 3.0.0)
10
+ activesupport (= 3.0.0)
11
+ arel (~> 1.0.0)
12
+ tzinfo (~> 0.3.23)
13
+ activesupport (3.0.0)
14
+ arel (1.0.1)
15
+ activesupport (~> 3.0.0)
16
+ builder (2.1.2)
17
+ cassandra (0.8.2)
18
+ json
19
+ rake
20
+ simple_uuid (>= 0.1.0)
21
+ thrift_client (>= 0.4.0)
22
+ flexmock (0.8.7)
23
+ i18n (0.4.1)
24
+ json (1.4.6)
25
+ nokogiri (1.4.3.1)
26
+ rake (0.8.7)
27
+ shoulda (2.11.3)
28
+ simple_uuid (0.1.1)
29
+ sqlite3-ruby (1.3.1)
30
+ thrift (0.2.0.4)
31
+ thrift_client (0.4.6)
32
+ thrift
33
+ tzinfo (0.3.23)
34
+ yajl-ruby (0.7.7)
35
+
36
+ PLATFORMS
37
+ ruby
38
+
39
+ DEPENDENCIES
40
+ activemodel (~> 3.0.0)
41
+ activerecord (~> 3.0.0)
42
+ cassandra (~> 0.8.2)
43
+ flexmock (~> 0.8.7)
44
+ nokogiri (~> 1.4.3.1)
45
+ shoulda (~> 2.11.3)
46
+ sqlite3-ruby
47
+ yajl-ruby (~> 0.7.7)
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2010 Adam Keys
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # Audit
2
+
3
+ Audit sits on top of your model objects and watches for changes to your data. When a change occurs, the differences are recorded and stored in Cassandra.
4
+
5
+ ## Usage
6
+
7
+ Include `Audit::Tracking` into your change-sensitive ActiveRecord models. When you make changes to data in those tables, the relevant details will be written to a Cassandra column family.
8
+
9
+ ## Example
10
+
11
+ >> require 'audit'
12
+ >> class User < ActiveRecord::Base; include Audit::Tracking; end
13
+ >> u = User.create(:name => 'Adam', :city => 'Dallas')
14
+ >> u.update_attributes(:city => 'Austin')
15
+ >> u.audits
16
+ [#<struct Audit::Changeset changes=[#<struct Audit::Change attribute="username", old="akk", new="therealadam">]>, #<struct Audit::Changeset changes=[#<struct Audit::Change attribute="username", old="adam", new="akk">]>, #<struct Audit::Changeset changes=[#<struct Audit::Change attribute="age", old=30, new=31>]>]
17
+
18
+ # Compatibility
19
+
20
+ Audit is tested against ActiveRecord 3.0, Ruby 1.8.7 and Ruby 1.9.2.
21
+
22
+ # Setup
23
+
24
+ For Cassandra 0.7, you can set up the schema with `cassandra-cli` like so:
25
+
26
+ /* Create a new keyspace */
27
+ create keyspace Audit with replication_factor = 1
28
+
29
+ /* Switch to the new keyspace */
30
+ use Audit
31
+
32
+ /* Create new column families */
33
+ create column family Audits with column_type = 'Standard' and comparator = 'TimeUUIDType' and rows_cached = 10000
34
+
35
+ For Cassandra 0.6, add the following to `storage-conf.xml`:
36
+
37
+ <Keyspace Name="Audit">
38
+ <KeysCachedFraction>0.01</KeysCachedFraction>
39
+ <ColumnFamily CompareWith="TimeUUIDType" Name="Audits" />
40
+ <ReplicaPlacementStrategy>org.apache.cassandra.locator.RackUnawareStrategy</ReplicaPlacementStrategy>
41
+ <ReplicationFactor>1</ReplicationFactor>
42
+ <EndPointSnitch>org.apache.cassandra.locator.EndPointSnitch</EndPointSnitch>
43
+ </Keyspace>
44
+
45
+ ## Hacking
46
+
47
+ Set up RVM:
48
+
49
+ $ rvm install ree-1.8.7-2010.01
50
+ $ rvm use ree-1.8.7-2010.01
51
+ $ rvm gemset create audit
52
+ $ rvm gemset use audit
53
+ $ gem install bundler
54
+ $ bundle install
55
+ $ rvm install 1.9.2
56
+ $ rvm use 1.9.2
57
+ $ rvm gemset create audit
58
+ $ rvm gemset use audit
59
+ $ gem install bundler
60
+ $ bundle install
61
+
62
+ Run the test suite with all supported runtimes:
63
+
64
+ $ rvm 1.9.2@audit,ree-1.8.7-2010.01@audit rake test
65
+
66
+ ## TODO
67
+
68
+ - Ignore changes on some attributes
69
+ - Add more AR callbacks (delete, ?)
70
+ - Generate bucket names for namespaced models
71
+
72
+ ## License
73
+
74
+ Copyright 2010 Adam Keys `<adam@therealaadam.com>`
75
+
76
+ Audit is MIT licensed. Enjoy!
data/Rakefile ADDED
@@ -0,0 +1,129 @@
1
+ require 'rake/testtask'
2
+ require 'date'
3
+
4
+ # Helpers
5
+ begin
6
+ def name
7
+ @name ||= Dir['*.gemspec'].first.split('.').first
8
+ end
9
+
10
+ def version
11
+ line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/]
12
+ line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1]
13
+ end
14
+
15
+ def date
16
+ Date.today.to_s
17
+ end
18
+
19
+ def rubyforge_project
20
+ name
21
+ end
22
+
23
+ def gemspec_file
24
+ "#{name}.gemspec"
25
+ end
26
+
27
+ def gem_file
28
+ "#{name}-#{version}.gem"
29
+ end
30
+
31
+ def replace_header(head, header_name)
32
+ head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"}
33
+ end
34
+ end
35
+
36
+ task :default => :test
37
+
38
+ Rake::TestTask.new do |t|
39
+ t.libs << "test"
40
+ t.test_files = FileList['test/*_test.rb']
41
+ end
42
+
43
+ desc "Run tests against all supported Ruby versions"
44
+ task :compat do
45
+ sh "rvm 1.9.2@audit,ree-1.8.7-2010.01@audit rake test"
46
+ end
47
+
48
+ desc "Generate RCov test coverage and open in your browser"
49
+ task :coverage do
50
+ require 'rcov'
51
+ sh "rm -fr coverage"
52
+ sh "rcov test/test_*.rb"
53
+ sh "open coverage/index.html"
54
+ end
55
+
56
+ require 'rake/rdoctask'
57
+ Rake::RDocTask.new do |rdoc|
58
+ rdoc.rdoc_dir = 'rdoc'
59
+ rdoc.title = "#{name} #{version}"
60
+ rdoc.rdoc_files.include('README.md')
61
+ rdoc.rdoc_files.include('lib/**/*.rb')
62
+ end
63
+
64
+ desc "Open an irb session preloaded with this library"
65
+ task :console do
66
+ sh "irb -rubygems -r ./lib/#{name}.rb"
67
+ end
68
+
69
+ # =============
70
+ # = Packaging =
71
+ # =============
72
+
73
+ task :release => :build do
74
+ unless `git branch` =~ /^\* master$/
75
+ puts "You must be on the master branch to release!"
76
+ exit!
77
+ end
78
+ sh "git commit --allow-empty -a -m 'Release #{version}'"
79
+ sh "git tag v#{version}"
80
+ sh "git push origin master"
81
+ sh "git push origin v#{version}"
82
+ sh "gem push pkg/#{name}-#{version}.gem"
83
+ end
84
+
85
+ task :build => :gemspec do
86
+ sh "mkdir -p pkg"
87
+ sh "gem build #{gemspec_file}"
88
+ sh "mv #{gem_file} pkg"
89
+ end
90
+
91
+ task :gemspec => :validate do
92
+ # read spec file and split out manifest section
93
+ spec = File.read(gemspec_file)
94
+ head, manifest, tail = spec.split(" # = MANIFEST =\n")
95
+
96
+ # replace name version and date
97
+ replace_header(head, :name)
98
+ replace_header(head, :version)
99
+ replace_header(head, :date)
100
+ #comment this out if your rubyforge_project has a different name
101
+ replace_header(head, :rubyforge_project)
102
+
103
+ # determine file list from git ls-files
104
+ files = `git ls-files`.
105
+ split("\n").
106
+ sort.
107
+ reject { |file| file =~ /^\./ }.
108
+ reject { |file| file =~ /^(rdoc|pkg)/ }.
109
+ map { |file| " #{file}" }.
110
+ join("\n")
111
+
112
+ # piece file back together and write
113
+ manifest = " s.files = %w[\n#{files}\n ]\n"
114
+ spec = [head, manifest, tail].join(" # = MANIFEST =\n")
115
+ File.open(gemspec_file, 'w') { |io| io.write(spec) }
116
+ puts "Updated #{gemspec_file}"
117
+ end
118
+
119
+ task :validate do
120
+ libfiles = Dir['lib/*'] - ["lib/#{name}.rb", "lib/#{name}"]
121
+ unless libfiles.empty?
122
+ puts "Directory `lib` should only contain a `#{name}.rb` file and `#{name}` dir."
123
+ exit!
124
+ end
125
+ unless Dir['VERSION*'].empty?
126
+ puts "A `VERSION` file at root level violates Gem best practices."
127
+ exit!
128
+ end
129
+ end
data/audit.gemspec ADDED
@@ -0,0 +1,80 @@
1
+ ## This is the rakegem gemspec template. Make sure you read and understand
2
+ ## all of the comments. Some sections require modification, and others can
3
+ ## be deleted if you don't need them. Once you understand the contents of
4
+ ## this file, feel free to delete any comments that begin with two hash marks.
5
+ ## You can find comprehensive Gem::Specification documentation, at
6
+ ## http://docs.rubygems.org/read/chapter/20
7
+ Gem::Specification.new do |s|
8
+ s.specification_version = 2 if s.respond_to? :specification_version=
9
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
10
+ s.rubygems_version = '1.3.5'
11
+
12
+ ## Leave these as is they will be modified for you by the rake gemspec task.
13
+ ## If your rubyforge_project name is different, then edit it and comment out
14
+ ## the sub! line in the Rakefile
15
+ s.name = 'audit'
16
+ s.version = '0.1.0'
17
+ s.date = '2010-09-29'
18
+ s.rubyforge_project = 'audit'
19
+
20
+ ## Make sure your summary is short. The description may be as long
21
+ ## as you like.
22
+ s.summary = "Audit logs changes to model objects to Cassandra."
23
+ s.description = "Audit sits on top of your model objects and watches for changes to your data. When a change occurs, the differences are recorded and stored in Cassandra."
24
+
25
+ ## List the primary authors. If there are a bunch of authors, it's probably
26
+ ## better to set the email to an email list or something. If you don't have
27
+ ## a custom homepage, consider using your GitHub URL or the like.
28
+ s.authors = ["Adam Keys"]
29
+ s.email = 'adam@therealadam.com'
30
+ s.homepage = 'http://github.com/therealadam/auditus'
31
+
32
+ ## This gets added to the $LOAD_PATH so that 'lib/NAME.rb' can be required as
33
+ ## require 'NAME.rb' or'/lib/NAME/file.rb' can be as require 'NAME/file.rb'
34
+ s.require_paths = %w[lib]
35
+
36
+ ## Specify any RDoc options here. You'll want to add your README and
37
+ ## LICENSE files to the extra_rdoc_files list.
38
+ s.rdoc_options = ["--charset=UTF-8"]
39
+ s.extra_rdoc_files = %w[README.md LICENSE]
40
+
41
+ ## List your runtime dependencies here. Runtime dependencies are those
42
+ ## that are needed for an end user to actually USE your code.
43
+ s.add_dependency('cassandra', ["~> 0.8.2"])
44
+
45
+ ## List your development dependencies here. Development dependencies are
46
+ ## those that are only needed during development
47
+ s.add_development_dependency('shoulda', ["~> 2.11.3"])
48
+ s.add_development_dependency('nokogiri', ['~> 1.4.3.1'])
49
+
50
+ ## Leave this section as-is. It will be automatically generated from the
51
+ ## contents of your Git repository via the gemspec task. DO NOT REMOVE
52
+ ## THE MANIFEST COMMENTS, they are used as delimiters by the task.
53
+ # = MANIFEST =
54
+ s.files = %w[
55
+ Gemfile
56
+ Gemfile.lock
57
+ LICENSE
58
+ README.md
59
+ Rakefile
60
+ audit.gemspec
61
+ examples/active_model.rb
62
+ examples/active_record.rb
63
+ examples/auditer.rb
64
+ examples/common.rb
65
+ lib/audit.rb
66
+ lib/audit/changeset.rb
67
+ lib/audit/log.rb
68
+ lib/audit/tracking.rb
69
+ test/changeset_test.rb
70
+ test/log_test.rb
71
+ test/storage-conf.xml
72
+ test/test_helper.rb
73
+ test/tracking_test.rb
74
+ ]
75
+ # = MANIFEST =
76
+
77
+ ## Test files will be grabbed from the file list. Make sure the path glob
78
+ ## matches what you actually use.
79
+ s.test_files = s.files.select { |path| path =~ /^test\/.*_test\.rb/ }
80
+ end
@@ -0,0 +1,42 @@
1
+ # TODO: write me
2
+
3
+ require 'common'
4
+ require 'audit'
5
+ require 'active_model'
6
+
7
+ class User
8
+ extend ActiveModel::Callbacks
9
+
10
+ define_model_callbacks :create, :update
11
+ after_update :bonk
12
+ after_create :clown
13
+
14
+ def self.create(attrs)
15
+ new.create(attrs)
16
+ end
17
+
18
+ def create(attrs)
19
+ _run_create_callbacks do
20
+ puts "Creating: #{attrs}"
21
+ end
22
+ end
23
+
24
+ def update(attrs)
25
+ _run_update_callbacks do
26
+ puts "Updating #{attrs}"
27
+ end
28
+ end
29
+
30
+ def bonk
31
+ puts 'bonk!'
32
+ end
33
+
34
+ def clown
35
+ puts "clown!"
36
+ end
37
+
38
+ end
39
+
40
+ if __FILE__ == $PROGRAM_NAME
41
+ User.create(:foo)
42
+ end
@@ -0,0 +1,37 @@
1
+ require 'common'
2
+ require 'pp'
3
+ require 'audit'
4
+ require 'active_record'
5
+
6
+ ActiveRecord::Base.establish_connection(
7
+ :adapter => 'sqlite3',
8
+ :database => ':memory:'
9
+ )
10
+
11
+ ActiveRecord::Schema.define do
12
+ create_table :users do |t|
13
+ t.string :username, :null => false
14
+ t.integer :age, :null => false
15
+ t.integer :gizmo
16
+ end
17
+ end
18
+
19
+ class User < ActiveRecord::Base
20
+ include Audit::Tracking
21
+ end
22
+
23
+ if __FILE__ == $PROGRAM_NAME
24
+ Audit::Log.connection = Cassandra.new('Audit')
25
+ Audit::Log.clear!
26
+
27
+ user = User.create(:username => 'adam', :age => 30)
28
+ user.update_attributes(:age => 31)
29
+ user.update_attributes(:username => 'akk')
30
+
31
+ user.audit_metadata(:reason => "Canonize usernames")
32
+ user.update_attributes(:username => 'therealadam')
33
+
34
+ 100.times.each { |i| user.update_attributes(:gizmo => i) }
35
+
36
+ pp user.audits
37
+ end
@@ -0,0 +1,11 @@
1
+ require 'common'
2
+ require 'audit'
3
+
4
+ if __FILE__ == $PROGRAM_NAME
5
+ log = Audit::Log
6
+ log.connection = Cassandra.new('Audit')
7
+
8
+ changes = {:age => [30, 31]}
9
+ log.record(:user, 1, changes)
10
+ p log.audits(:user, 1)
11
+ end
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler.setup
data/lib/audit.rb ADDED
@@ -0,0 +1,12 @@
1
+ # Audit is a system for tracking model changes outside of your application's
2
+ # database.
3
+ module Audit
4
+
5
+ # Everything needs a version.
6
+ VERSION = '0.1.0'
7
+
8
+ autoload :Log, "audit/log"
9
+ autoload :Changeset, "audit/changeset"
10
+ autoload :Tracking, "audit/tracking"
11
+
12
+ end
@@ -0,0 +1,44 @@
1
+ # A structure for tracking individual changes to a record.
2
+ Audit::Change = Struct.new(:attribute, :old_value, :new_value)
3
+
4
+ # A structure for tracking an atomic group of changes to a model.
5
+ class Audit::Changeset < Struct.new(:changes, :metadata)
6
+
7
+ # Recreate a changeset given change data as generated by ActiveRecord.
8
+ #
9
+ # hsh - the Hash to convert to a Changeset. Recognizes two keys:
10
+ # "changes" - a Hash of changes as generated by ActiveRecord
11
+ # "metadata" - user-provided metadata regarding this change
12
+ #
13
+ # Examples:
14
+ #
15
+ # Audit::Changeset.from_hash({"changes" => {'age' => [30, 31]}})
16
+ # # [<struct Audit::Changeset @attribute="age" @old_value=30
17
+ # # @new_value=31>]
18
+ #
19
+ # Returns an Array of Changeset objects, one for each changed attribute
20
+ def self.from_hash(hsh)
21
+ changes = hsh["changes"].map do |k, v|
22
+ attribute = k
23
+ old_value = v.first
24
+ new_value = v.last
25
+ Audit::Change.new(attribute, old_value, new_value)
26
+ end
27
+ new(changes, hsh["metadata"])
28
+ end
29
+
30
+ # Recreate a changeset given one or more stored audit records.
31
+ #
32
+ # enum - an Array of change Hashes (see `from_hash` for details)
33
+ #
34
+ # Returns an Array of Changeset objects, one for each atomic change
35
+ def self.from_enumerable(enum)
36
+ case enum
37
+ when Hash
38
+ from_hash(enum)
39
+ when Array
40
+ enum.map { |hsh| from_hash(hsh) }
41
+ end
42
+ end
43
+
44
+ end
data/lib/audit/log.rb ADDED
@@ -0,0 +1,51 @@
1
+ require 'cassandra'
2
+ require 'active_support/core_ext/module'
3
+ require 'simple_uuid'
4
+ require 'yajl'
5
+
6
+ # Methods for manipulating audit data stored in Cassandra.
7
+ module Audit::Log
8
+
9
+ # Public: set or fetch the connection to Cassandra that Audit will use.
10
+ mattr_accessor :connection
11
+
12
+ # Store an audit record.
13
+ #
14
+ # bucket - the String name for the logical bucket this audit record belongs
15
+ # to (i.e. table)
16
+ # key - the String key into the logical bucket
17
+ # changes - the changes hash (as generated by ActiveRecord) to store
18
+ #
19
+ # Returns nothing.
20
+ def self.record(bucket, key, changes)
21
+ json = Yajl::Encoder.encode(changes)
22
+ payload = {SimpleUUID::UUID.new => json}
23
+ connection.insert(:Audits, "#{bucket}:#{key}", payload)
24
+ end
25
+
26
+ # Fetch all audits for a given record.
27
+ #
28
+ # bucket - the String name for the logical bucket this audit record belongs
29
+ # to (i.e. table)
30
+ # key - the String key into the logical bucket
31
+ #
32
+ # Returns an Array of Changeset objects
33
+ def self.audits(bucket, key)
34
+ # TODO: figure out how to do pagination here
35
+ payload = connection.get(:Audits, "#{bucket}:#{key}", :reversed => true)
36
+ payload.values.map do |p|
37
+ Audit::Changeset.from_enumerable(Yajl::Parser.parse(p))
38
+ end
39
+ end
40
+
41
+ # Clear all audit data.
42
+ # Note that this doesn't yet operate on logical
43
+ # buckets. _All_ of the audit data is destroyed. Proceed with caution.
44
+ #
45
+ # Returns nothing.
46
+ def self.clear!
47
+ # It'd be nice if this could clear one bucket at a time
48
+ connection.clear_keyspace!
49
+ end
50
+
51
+ end
@@ -0,0 +1,52 @@
1
+ require 'active_support/core_ext/module'
2
+
3
+ # Methods for tracking changes to your models by creating audit records
4
+ # for every atomic change. Including this module adds callbacks which create
5
+ # audit records every time a model object is changed and saved.
6
+ module Audit::Tracking
7
+ extend ActiveSupport::Concern
8
+
9
+ # Public: set the log object to track changes with.
10
+ #
11
+ # Returns the log object currently in use.
12
+ mattr_accessor :log
13
+ self.log = Audit::Log
14
+
15
+ included do
16
+ before_update :audit
17
+ end
18
+
19
+ # Public: fetch audit records for a model object.
20
+ #
21
+ # Returns an Array of Changeset objects.
22
+ def audits
23
+ Audit::Tracking.log.audits(audit_bucket, self.id)
24
+ end
25
+
26
+ # Creates a new audit record for this model object using data returned by
27
+ # ActiveRecord::Base#changes.
28
+ #
29
+ # Returns nothing.
30
+ def audit
31
+ data = {"changes" => changes, "metadata" => audit_metadata}
32
+ Audit::Tracking.log.record(audit_bucket, self.id, data)
33
+ @audit_metadata = {}
34
+ end
35
+
36
+ # Generates the bucket name for the model class.
37
+ #
38
+ # Returns a Symbol-ified and pluralized version of the model's name.
39
+ def audit_bucket
40
+ self.class.name.pluralize.to_sym
41
+ end
42
+
43
+ # Public: Store audit metadata for the next write.
44
+ #
45
+ # metadata - a Hash of data that is written alongside the change data
46
+ #
47
+ # Returns nothing.
48
+ def audit_metadata(metadata={})
49
+ @audit_metadata = @audit_metadata.try(:update, metadata) || metadata
50
+ end
51
+
52
+ end
@@ -0,0 +1,48 @@
1
+ require 'test_helper'
2
+
3
+ class ChangesetTest < Test::Unit::TestCase
4
+
5
+ should 'convert a hash of changes to a changeset' do
6
+ metadata = {
7
+ "reason" => "Canonize usernames, getting older",
8
+ "signed" => "akk"
9
+ }
10
+ changes = {
11
+ "changes" => {
12
+ "username" => ["akk", "adam"],
13
+ "age" => [30, 31]
14
+ },
15
+ "metadata" => metadata
16
+ }
17
+ changeset = Audit::Changeset.from_enumerable(changes)
18
+
19
+ assert_equal 2, changeset.changes.length
20
+ assert(changeset.changes.all? { |cs|
21
+ %w{username age}.include?(cs.attribute)
22
+ ["akk", 30].include?(cs.old_value)
23
+ ["adam", 31].include?(cs.new_value)
24
+ })
25
+ assert_equal metadata, changeset.metadata
26
+ end
27
+
28
+ should "convert multile change records to an Array of Changesets" do
29
+ changes = [
30
+ {
31
+ "changes" => {"username" => ["akk", "adam"], "age" => [30, 31]},
32
+ "metadata" => {}
33
+ },
34
+ {
35
+ "changes" => {
36
+ "username" => ["adam", "therealadam"],
37
+ "age" => [31, 32]
38
+ },
39
+ "metadata" => {}
40
+ }
41
+
42
+ ]
43
+ changesets = Audit::Changeset.from_enumerable(changes)
44
+
45
+ assert_equal 2, changesets.length
46
+ end
47
+
48
+ end
data/test/log_test.rb ADDED
@@ -0,0 +1,35 @@
1
+ require 'test_helper'
2
+
3
+ class LogTest < Test::Unit::TestCase
4
+
5
+ should "save audit record" do
6
+ assert Audit::Log.record(:Users, 1, simple_change)
7
+ end
8
+
9
+ should "load audit records" do
10
+ Audit::Log.record(:Users, 1, simple_change)
11
+ assert_kind_of Audit::Changeset, Audit::Log.audits(:Users, 1).first
12
+ end
13
+
14
+ should "load audits with multiple changed attributes" do
15
+ Audit::Log.record(:Users, 1, multiple_changes)
16
+ changes = Audit::Log.audits(:Users, 1).first.changes
17
+ changes.each do |change|
18
+ assert %w{username age}.include?(change.attribute)
19
+ assert ["akk", 30].include?(change.old_value)
20
+ assert ["adam", 31].include?(change.new_value)
21
+ end
22
+ end
23
+
24
+ def simple_change
25
+ {"changes" => {"username" => ["akk", "adam"]}, "metadata" => {}}
26
+ end
27
+
28
+ def multiple_changes
29
+ {
30
+ "changes" => {"username" => ["akk", "adam"], "age" => [30, 31]},
31
+ "metadata" => {}
32
+ }
33
+ end
34
+
35
+ end
@@ -0,0 +1,10 @@
1
+ <!-- This isn't a valid config, just enough to get tests passing -->
2
+ <Keyspaces>
3
+ <Keyspace Name="Audit">
4
+ <KeysCachedFraction>0.01</KeysCachedFraction>
5
+ <ColumnFamily CompareWith="UTF8Type" Name="Audits" />
6
+ <ReplicaPlacementStrategy>org.apache.cassandra.locator.RackUnawareStrategy</ReplicaPlacementStrategy>
7
+ <ReplicationFactor>1</ReplicationFactor>
8
+ <EndPointSnitch>org.apache.cassandra.locator.EndPointSnitch</EndPointSnitch>
9
+ </Keyspace>
10
+ </Keyspaces>
@@ -0,0 +1,24 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ Bundler.setup
4
+
5
+ require 'test/unit'
6
+ require 'shoulda'
7
+ require 'flexmock/test_unit'
8
+ require 'cassandra'
9
+ require 'cassandra/mock'
10
+ require 'active_record'
11
+ require 'audit'
12
+
13
+ class Test::Unit::TestCase
14
+
15
+ alias_method :original_setup, :setup
16
+ def setup
17
+ Audit::Log.connection = Cassandra::Mock.new(
18
+ 'Audit',
19
+ File.join(File.dirname(__FILE__), 'storage-conf.xml')
20
+ )
21
+ original_setup
22
+ end
23
+
24
+ end
@@ -0,0 +1,62 @@
1
+ require "test_helper"
2
+
3
+ ActiveRecord::Base.establish_connection(
4
+ :adapter => 'sqlite3',
5
+ :database => ':memory:'
6
+ )
7
+
8
+ ActiveRecord::Schema.define do
9
+ create_table :users do |t|
10
+ t.string :username, :null => false
11
+ t.integer :age, :null => false
12
+ end
13
+ end
14
+
15
+ class User < ActiveRecord::Base; include Audit::Tracking; end
16
+
17
+ class TrackingTest < Test::Unit::TestCase
18
+
19
+ def setup
20
+ super
21
+ @model = User.new
22
+ end
23
+
24
+ context "generate an audit bucket name" do
25
+
26
+ should "based on the model name" do
27
+ assert_equal :Users, @model.audit_bucket
28
+ end
29
+
30
+ end
31
+
32
+ should "track audit metadata for the next save" do
33
+ audit_metadata = {"reason" => "Canonize usernames", "changed_by" => "JD"}
34
+ user = User.create(:username => "adam", :age => 31)
35
+ user.audit_metadata(audit_metadata)
36
+ user.update_attributes(:username => "therealadam")
37
+ changes = user.audits
38
+
39
+ assert_equal audit_metadata, changes.first.metadata
40
+
41
+ user.save!
42
+
43
+ assert_equal({}, user.audit_metadata) # Should clear audit after write
44
+ end
45
+
46
+ should "add audit-related methods" do
47
+ assert_equal %w{audit audit_bucket audit_metadata audits},
48
+ @model.methods.map { |s| s.to_s }.grep(/audit/).sort
49
+ end
50
+
51
+ should "set the log object to an arbitrary object" do
52
+ Audit::Tracking.log = flexmock(:log).
53
+ should_receive(:audits).
54
+ once.
55
+ mock
56
+
57
+ User.create(:username => "Adam", :age => "31").audits
58
+
59
+ Audit::Tracking.log = Audit::Log
60
+ end
61
+
62
+ end
metadata ADDED
@@ -0,0 +1,125 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: audit
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Adam Keys
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-09-29 00:00:00 -05:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: cassandra
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ~>
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ - 8
30
+ - 2
31
+ version: 0.8.2
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: shoulda
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ~>
40
+ - !ruby/object:Gem::Version
41
+ segments:
42
+ - 2
43
+ - 11
44
+ - 3
45
+ version: 2.11.3
46
+ type: :development
47
+ version_requirements: *id002
48
+ - !ruby/object:Gem::Dependency
49
+ name: nokogiri
50
+ prerelease: false
51
+ requirement: &id003 !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ~>
54
+ - !ruby/object:Gem::Version
55
+ segments:
56
+ - 1
57
+ - 4
58
+ - 3
59
+ - 1
60
+ version: 1.4.3.1
61
+ type: :development
62
+ version_requirements: *id003
63
+ description: Audit sits on top of your model objects and watches for changes to your data. When a change occurs, the differences are recorded and stored in Cassandra.
64
+ email: adam@therealadam.com
65
+ executables: []
66
+
67
+ extensions: []
68
+
69
+ extra_rdoc_files:
70
+ - README.md
71
+ - LICENSE
72
+ files:
73
+ - Gemfile
74
+ - Gemfile.lock
75
+ - LICENSE
76
+ - README.md
77
+ - Rakefile
78
+ - audit.gemspec
79
+ - examples/active_model.rb
80
+ - examples/active_record.rb
81
+ - examples/auditer.rb
82
+ - examples/common.rb
83
+ - lib/audit.rb
84
+ - lib/audit/changeset.rb
85
+ - lib/audit/log.rb
86
+ - lib/audit/tracking.rb
87
+ - test/changeset_test.rb
88
+ - test/log_test.rb
89
+ - test/storage-conf.xml
90
+ - test/test_helper.rb
91
+ - test/tracking_test.rb
92
+ has_rdoc: true
93
+ homepage: http://github.com/therealadam/auditus
94
+ licenses: []
95
+
96
+ post_install_message:
97
+ rdoc_options:
98
+ - --charset=UTF-8
99
+ require_paths:
100
+ - lib
101
+ required_ruby_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ segments:
106
+ - 0
107
+ version: "0"
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ segments:
113
+ - 0
114
+ version: "0"
115
+ requirements: []
116
+
117
+ rubyforge_project: audit
118
+ rubygems_version: 1.3.6
119
+ signing_key:
120
+ specification_version: 2
121
+ summary: Audit logs changes to model objects to Cassandra.
122
+ test_files:
123
+ - test/changeset_test.rb
124
+ - test/log_test.rb
125
+ - test/tracking_test.rb