audit 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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